Skip to content

Commit 04a655c

Browse files
mbooth101dpgeorge
authored andcommitted
extmod/modframebuf: Add polygon drawing methods.
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]>
1 parent 42ec970 commit 04a655c

File tree

4 files changed

+931
-2
lines changed

4 files changed

+931
-2
lines changed

docs/library/framebuf.rst

+13-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class FrameBuffer
1111
-----------------
1212

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

1717
For example::
1818

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

101+
.. method:: FrameBuffer.poly(x, y, coords, c[, f])
102+
103+
Given a list of coordinates, draw an arbitrary (convex or concave) closed
104+
polygon at the given x, y location using the given color.
105+
106+
The *coords* must be specified as a :mod:`array` of integers, e.g.
107+
``array('h', [x0, y0, x1, y1, ... xn, yn])``.
108+
109+
The optional *f* parameter can be set to ``True`` to fill the polygon.
110+
Otherwise just a one pixel outline is drawn.
111+
101112
Drawing text
102113
------------
103114

extmod/modframebuf.c

+114
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <string.h>
2929

3030
#include "py/runtime.h"
31+
#include "py/binary.h"
3132

3233
#if MICROPY_PY_FRAMEBUF
3334

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

567+
#if MICROPY_PY_ARRAY && !MICROPY_ENABLE_DYNRUNTIME
568+
// TODO: poly needs mp_binary_get_size & mp_binary_get_val_array which aren't
569+
// available in dynruntime.h yet.
570+
571+
STATIC mp_int_t poly_int(mp_buffer_info_t *bufinfo, size_t index) {
572+
return mp_obj_get_int(mp_binary_get_val_array(bufinfo->typecode, bufinfo->buf, index));
573+
}
574+
575+
STATIC mp_obj_t framebuf_poly(size_t n_args, const mp_obj_t *args_in) {
576+
mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args_in[0]);
577+
578+
mp_int_t x = mp_obj_get_int(args_in[1]);
579+
mp_int_t y = mp_obj_get_int(args_in[2]);
580+
581+
mp_buffer_info_t bufinfo;
582+
mp_get_buffer_raise(args_in[3], &bufinfo, MP_BUFFER_READ);
583+
// If an odd number of values was given, this rounds down to multiple of two.
584+
int n_poly = bufinfo.len / (mp_binary_get_size('@', bufinfo.typecode, NULL) * 2);
585+
586+
if (n_poly == 0) {
587+
return mp_const_none;
588+
}
589+
590+
mp_int_t col = mp_obj_get_int(args_in[4]);
591+
bool fill = n_args > 5 && mp_obj_is_true(args_in[5]);
592+
593+
if (fill) {
594+
// This implements an integer version of http://alienryderflex.com/polygon_fill/
595+
596+
// The idea is for each scan line, compute the sorted list of x
597+
// coordinates where the scan line intersects the polygon edges,
598+
// then fill between each resulting pair.
599+
600+
// Restrict just to the scan lines that include the vertical extent of
601+
// this polygon.
602+
mp_int_t y_min = INT_MAX, y_max = INT_MIN;
603+
for (int i = 0; i < n_poly; i++) {
604+
mp_int_t py = poly_int(&bufinfo, i * 2 + 1);
605+
y_min = MIN(y_min, py);
606+
y_max = MAX(y_max, py);
607+
}
608+
609+
for (mp_int_t row = y_min; row <= y_max; row++) {
610+
// Each node is the x coordinate where an edge crosses this scan line.
611+
mp_int_t nodes[n_poly];
612+
int n_nodes = 0;
613+
mp_int_t px1 = poly_int(&bufinfo, 0);
614+
mp_int_t py1 = poly_int(&bufinfo, 1);
615+
int i = n_poly * 2 - 1;
616+
do {
617+
mp_int_t py2 = poly_int(&bufinfo, i--);
618+
mp_int_t px2 = poly_int(&bufinfo, i--);
619+
620+
// Don't include the bottom pixel of a given edge to avoid
621+
// duplicating the node with the start of the next edge. This
622+
// will miss some pixels on the boundary, but we get them at
623+
// the end when we unconditionally draw the outline.
624+
if (py1 != py2 && ((py1 > row && py2 <= row) || (py1 <= row && py2 > row))) {
625+
mp_int_t node = (32 * px1 + 32 * (px2 - px1) * (row - py1) / (py2 - py1) + 16) / 32;
626+
nodes[n_nodes++] = node;
627+
}
628+
629+
px1 = px2;
630+
py1 = py2;
631+
} while (i >= 0);
632+
633+
if (!n_nodes) {
634+
continue;
635+
}
636+
637+
// Sort the nodes left-to-right (bubble-sort for code size).
638+
i = 0;
639+
while (i < n_nodes - 1) {
640+
if (nodes[i] > nodes[i + 1]) {
641+
mp_int_t swap = nodes[i];
642+
nodes[i] = nodes[i + 1];
643+
nodes[i + 1] = swap;
644+
if (i) {
645+
i--;
646+
}
647+
} else {
648+
i++;
649+
}
650+
}
651+
652+
// Fill between each pair of nodes.
653+
for (i = 0; i < n_nodes; i += 2) {
654+
fill_rect(self, x + nodes[i], y + row, (nodes[i + 1] - nodes[i]) + 1, 1, col);
655+
}
656+
}
657+
}
658+
659+
// Always draw the outline (either because fill=False, or to fix the
660+
// boundary pixels for a fill, see above).
661+
mp_int_t px1 = poly_int(&bufinfo, 0);
662+
mp_int_t py1 = poly_int(&bufinfo, 1);
663+
int i = n_poly * 2 - 1;
664+
do {
665+
mp_int_t py2 = poly_int(&bufinfo, i--);
666+
mp_int_t px2 = poly_int(&bufinfo, i--);
667+
line(self, x + px1, y + py1, x + px2, y + py2, col);
668+
px1 = px2;
669+
py1 = py2;
670+
} while (i >= 0);
671+
672+
return mp_const_none;
673+
}
674+
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_poly_obj, 5, 6, framebuf_poly);
675+
#endif // MICROPY_PY_ARRAY && !MICROPY_ENABLE_DYNRUNTIME
676+
566677
STATIC mp_obj_t framebuf_blit(size_t n_args, const mp_obj_t *args_in) {
567678
mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args_in[0]);
568679
mp_obj_t source_in = mp_obj_cast_to_native_base(args_in[1], MP_OBJ_FROM_PTR(&mp_type_framebuf));
@@ -698,6 +809,9 @@ STATIC const mp_rom_map_elem_t framebuf_locals_dict_table[] = {
698809
{ MP_ROM_QSTR(MP_QSTR_rect), MP_ROM_PTR(&framebuf_rect_obj) },
699810
{ MP_ROM_QSTR(MP_QSTR_line), MP_ROM_PTR(&framebuf_line_obj) },
700811
{ MP_ROM_QSTR(MP_QSTR_ellipse), MP_ROM_PTR(&framebuf_ellipse_obj) },
812+
#if MICROPY_PY_ARRAY
813+
{ MP_ROM_QSTR(MP_QSTR_poly), MP_ROM_PTR(&framebuf_poly_obj) },
814+
#endif
701815
{ MP_ROM_QSTR(MP_QSTR_blit), MP_ROM_PTR(&framebuf_blit_obj) },
702816
{ MP_ROM_QSTR(MP_QSTR_scroll), MP_ROM_PTR(&framebuf_scroll_obj) },
703817
{ MP_ROM_QSTR(MP_QSTR_text), MP_ROM_PTR(&framebuf_text_obj) },

tests/extmod/framebuf_polygon.py

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import sys
2+
3+
try:
4+
import framebuf
5+
from array import array
6+
except ImportError:
7+
print("SKIP")
8+
raise SystemExit
9+
10+
11+
# TODO: poly needs functions that aren't in dynruntime.h yet.
12+
if not hasattr(framebuf.FrameBuffer, "poly"):
13+
print("SKIP")
14+
raise SystemExit
15+
16+
17+
def print_buffer(buffer, width, height):
18+
for row in range(height):
19+
for col in range(width):
20+
val = buffer[(row * width) + col]
21+
sys.stdout.write(" {:02x}".format(val) if val else " ··")
22+
sys.stdout.write("\n")
23+
24+
25+
buf = bytearray(70 * 70)
26+
27+
w = 30
28+
h = 25
29+
fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
30+
col = 0xFF
31+
col_fill = 0x99
32+
33+
# This describes a arbitrary polygon (this happens to be a concave polygon in
34+
# the shape of an upper-case letter 'M').
35+
poly = array(
36+
"h",
37+
(
38+
0,
39+
20,
40+
3,
41+
20,
42+
3,
43+
10,
44+
6,
45+
17,
46+
9,
47+
10,
48+
9,
49+
20,
50+
12,
51+
20,
52+
12,
53+
3,
54+
9,
55+
3,
56+
6,
57+
10,
58+
3,
59+
3,
60+
0,
61+
3,
62+
),
63+
)
64+
# This describes the same polygon, but the points are in reverse order
65+
# (it shouldn't matter if the polygon has clockwise or anti-clockwise
66+
# winding). Also defined as a bytes instead of array.
67+
poly_reversed = bytes(
68+
(
69+
0,
70+
3,
71+
3,
72+
3,
73+
6,
74+
10,
75+
9,
76+
3,
77+
12,
78+
3,
79+
12,
80+
20,
81+
9,
82+
20,
83+
9,
84+
10,
85+
6,
86+
17,
87+
3,
88+
10,
89+
3,
90+
20,
91+
0,
92+
20,
93+
)
94+
)
95+
96+
# Draw the line polygon (at the origin) and the reversed-order polygon (offset).
97+
fbuf.fill(0)
98+
fbuf.poly(0, 0, poly, col)
99+
fbuf.poly(15, -2, poly_reversed, col)
100+
print_buffer(buf, w, h)
101+
print()
102+
103+
# Same but filled.
104+
fbuf.fill(0)
105+
fbuf.poly(0, 0, poly, col_fill, True)
106+
fbuf.poly(15, -2, poly_reversed, col_fill, True)
107+
print_buffer(buf, w, h)
108+
print()
109+
110+
# Draw the fill then the outline to ensure that no fill goes outside the outline.
111+
fbuf.fill(0)
112+
fbuf.poly(0, 0, poly, col_fill, True)
113+
fbuf.poly(0, 0, poly, col)
114+
fbuf.poly(15, -2, poly, col_fill, True)
115+
fbuf.poly(15, -2, poly, col)
116+
print_buffer(buf, w, h)
117+
print()
118+
119+
# Draw the outline then the fill to ensure the fill completely covers the outline.
120+
fbuf.fill(0)
121+
fbuf.poly(0, 0, poly, col)
122+
fbuf.poly(0, 0, poly, col_fill, True)
123+
fbuf.poly(15, -2, poly, col)
124+
fbuf.poly(15, -2, poly, col_fill, True)
125+
print_buffer(buf, w, h)
126+
print()
127+
128+
# Draw polygons that will go out of bounds at each of the edges.
129+
for x, y in (
130+
(
131+
-8,
132+
-8,
133+
),
134+
(
135+
24,
136+
-6,
137+
),
138+
(
139+
20,
140+
12,
141+
),
142+
(
143+
-2,
144+
10,
145+
),
146+
):
147+
fbuf.fill(0)
148+
fbuf.poly(x, y, poly, col)
149+
print_buffer(buf, w, h)
150+
print()
151+
fbuf.fill(0)
152+
fbuf.poly(x, y, poly_reversed, col, True)
153+
print_buffer(buf, w, h)
154+
print()
155+
156+
# Edge cases: These two lists describe self-intersecting polygons
157+
poly_hourglass = array("h", (0, 0, 9, 0, 0, 19, 9, 19))
158+
poly_star = array("h", (7, 0, 3, 18, 14, 5, 0, 5, 11, 18))
159+
160+
# As before, fill then outline.
161+
fbuf.fill(0)
162+
fbuf.poly(0, 2, poly_hourglass, col_fill, True)
163+
fbuf.poly(0, 2, poly_hourglass, col)
164+
fbuf.poly(12, 2, poly_star, col_fill, True)
165+
fbuf.poly(12, 2, poly_star, col)
166+
print_buffer(buf, w, h)
167+
print()
168+
169+
# Outline then fill.
170+
fbuf.fill(0)
171+
fbuf.poly(0, 2, poly_hourglass, col)
172+
fbuf.poly(0, 2, poly_hourglass, col_fill, True)
173+
fbuf.poly(12, 2, poly_star, col)
174+
fbuf.poly(12, 2, poly_star, col_fill, True)
175+
print_buffer(buf, w, h)
176+
print()
177+
178+
# Edge cases: These are "degenerate" polygons.
179+
poly_empty = array("h") # Will draw nothing at all.
180+
poly_one = array("h", (20, 20)) # Will draw a single point.
181+
poly_two = array("h", (10, 10, 5, 5)) # Will draw a single line.
182+
poly_wrong_length = array("h", (2, 2, 4)) # Will round down to one point.
183+
184+
fbuf.fill(0)
185+
fbuf.poly(0, 0, poly_empty, col)
186+
fbuf.poly(0, 0, poly_one, col)
187+
fbuf.poly(0, 0, poly_two, col)
188+
fbuf.poly(0, 0, poly_wrong_length, col)
189+
print_buffer(buf, w, h)
190+
print()
191+
192+
# A shape with a horizontal overhang.
193+
poly_overhang = array("h", (0, 0, 0, 5, 5, 5, 5, 10, 10, 10, 10, 0))
194+
195+
fbuf.fill(0)
196+
fbuf.poly(0, 0, poly_overhang, col)
197+
fbuf.poly(0, 0, poly_overhang, col_fill, True)
198+
print_buffer(buf, w, h)
199+
print()
200+
201+
fbuf.fill(0)
202+
fbuf.poly(0, 0, poly_overhang, col_fill, True)
203+
fbuf.poly(0, 0, poly_overhang, col)
204+
print_buffer(buf, w, h)
205+
print()
206+
207+
# Triangles
208+
w = 70
209+
h = 70
210+
fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
211+
t1 = array("h", [40, 0, 20, 68, 62, 40])
212+
t2 = array("h", [40, 0, 0, 16, 20, 68])
213+
214+
fbuf.fill(0)
215+
fbuf.poly(0, 0, t1, 0xFF, False)
216+
fbuf.poly(0, 0, t2, 0xFF, False)
217+
print_buffer(buf, w, h)
218+
219+
fbuf.fill(0)
220+
fbuf.poly(0, 0, t1, 0xFF, True)
221+
fbuf.poly(0, 0, t2, 0xFF, True)
222+
print_buffer(buf, w, h)

0 commit comments

Comments
 (0)