From 3eb72887a3ebf863a79322d7474b5296ad2fd4b9 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Wed, 13 Jul 2022 21:09:51 +0100 Subject: [PATCH] extmod/framebuf: Add polygon drawing methods. Add methods for drawing filled, and non-filled, polygons. poly(x, y, coords, col) * 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 fill_poly(x, y, coords, col) * Render filled arbitrary, closed, polygons 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 --- docs/library/framebuf.rst | 15 +- extmod/modframebuf.c | 202 ++++++++++++++++++++++++--- tests/extmod/framebuf_polygon.py | 160 +++++++++++++++++++++ tests/extmod/framebuf_polygon.py.exp | 132 +++++++++++++++++ 4 files changed, 491 insertions(+), 18 deletions(-) create mode 100644 tests/extmod/framebuf_polygon.py create mode 100644 tests/extmod/framebuf_polygon.py.exp diff --git a/docs/library/framebuf.rst b/docs/library/framebuf.rst index 098ada8153847..53ab61cce4046 100644 --- a/docs/library/framebuf.rst +++ b/docs/library/framebuf.rst @@ -11,8 +11,8 @@ class FrameBuffer ----------------- The FrameBuffer class provides a pixel buffer which can be drawn upon with -pixels, lines, rectangles, text and even other FrameBuffer's. It is useful -when generating output for displays. +pixels, lines, rectangles, polygons, text and even other FrameBuffers. It is +useful when generating output for displays. For example:: @@ -84,6 +84,17 @@ The following methods draw shapes onto the FrameBuffer. method draws only a 1 pixel outline whereas the `fill_rect` method draws both the outline and interior. +.. method:: FrameBuffer.poly(x, y, coords, c) +.. method:: FrameBuffer.fill_poly(x, y, coords, c) + + Given a list of coordinates, draw an arbitrary (convex or concave) closed + polygon at the given x, y location using the given color. The `poly` + method draws polygon edges as 1 pixel lines whereas the `fill_poly` method + draws filled polygons. For both methods, *coords* may be specified as a + list of x, y tuples, e.g. ``[(x0, y0), (x1, y1), ... (xn, yn)]`` or as an + :mod:`array`, e.g. ``array('h', [x0, y0, x1, y1, ... xn, yn])``. + + Drawing text ------------ diff --git a/extmod/modframebuf.c b/extmod/modframebuf.c index 5b6575d5a6503..4efd6429f6fc4 100644 --- a/extmod/modframebuf.c +++ b/extmod/modframebuf.c @@ -28,6 +28,7 @@ #include #include "py/runtime.h" +#include "py/binary.h" #if MICROPY_PY_FRAMEBUF @@ -404,16 +405,7 @@ STATIC mp_obj_t framebuf_rect(size_t n_args, const mp_obj_t *args) { } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_rect_obj, 6, 6, framebuf_rect); -STATIC mp_obj_t framebuf_line(size_t n_args, const mp_obj_t *args) { - (void)n_args; - - mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args[0]); - mp_int_t x1 = mp_obj_get_int(args[1]); - mp_int_t y1 = mp_obj_get_int(args[2]); - mp_int_t x2 = mp_obj_get_int(args[3]); - mp_int_t y2 = mp_obj_get_int(args[4]); - mp_int_t col = mp_obj_get_int(args[5]); - +STATIC void line(const mp_obj_framebuf_t *fb, mp_int_t x1, mp_int_t y1, mp_int_t x2, mp_int_t y2, mp_int_t col) { mp_int_t dx = x2 - x1; mp_int_t sx; if (dx > 0) { @@ -452,12 +444,12 @@ STATIC mp_obj_t framebuf_line(size_t n_args, const mp_obj_t *args) { mp_int_t e = 2 * dy - dx; for (mp_int_t i = 0; i < dx; ++i) { if (steep) { - if (0 <= y1 && y1 < self->width && 0 <= x1 && x1 < self->height) { - setpixel(self, y1, x1, col); + if (0 <= y1 && y1 < fb->width && 0 <= x1 && x1 < fb->height) { + setpixel(fb, y1, x1, col); } } else { - if (0 <= x1 && x1 < self->width && 0 <= y1 && y1 < self->height) { - setpixel(self, x1, y1, col); + if (0 <= x1 && x1 < fb->width && 0 <= y1 && y1 < fb->height) { + setpixel(fb, x1, y1, col); } } while (e >= 0) { @@ -468,14 +460,190 @@ STATIC mp_obj_t framebuf_line(size_t n_args, const mp_obj_t *args) { e += 2 * dy; } - if (0 <= x2 && x2 < self->width && 0 <= y2 && y2 < self->height) { - setpixel(self, x2, y2, col); + if (0 <= x2 && x2 < fb->width && 0 <= y2 && y2 < fb->height) { + setpixel(fb, x2, y2, col); } +} + +STATIC mp_obj_t framebuf_line(size_t n_args, const mp_obj_t *args) { + (void)n_args; + + mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args[0]); + mp_int_t x1 = mp_obj_get_int(args[1]); + mp_int_t y1 = mp_obj_get_int(args[2]); + mp_int_t x2 = mp_obj_get_int(args[3]); + mp_int_t y2 = mp_obj_get_int(args[4]); + mp_int_t col = mp_obj_get_int(args[5]); + + line(self, x1, y1, x2, y2, col); return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_line_obj, 6, 6, framebuf_line); +STATIC mp_obj_t framebuf_poly(size_t n_args, const mp_obj_t *args) { + (void)n_args; + + mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args[0]); + + mp_int_t x = mp_obj_get_int(args[1]); + mp_int_t y = mp_obj_get_int(args[2]); + + size_t poly_len; + mp_buffer_info_t bufinfo; + mp_obj_t *polygon; + + bool is_buffer = mp_get_buffer(args[3], &bufinfo, MP_BUFFER_READ); + if (is_buffer) { + poly_len = bufinfo.len / (mp_binary_get_size('@', bufinfo.typecode, NULL) * 2); + } else { + mp_obj_get_array(args[3], &poly_len, &polygon); + } + + if (poly_len == 0) { + return mp_const_none; + } + + mp_int_t col = mp_obj_get_int(args[4]); + + mp_int_t px1, py1, px2, py2; + + if (is_buffer) { + px1 = x + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, (poly_len - 1) * 2)); + py1 = y + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, (poly_len - 1) * 2 + 1)); + } else { + mp_obj_t *point; + mp_obj_get_array_fixed_n(polygon[poly_len - 1], 2, &point); + px1 = x + mp_obj_get_int(point[0]); + py1 = y + mp_obj_get_int(point[1]); + } + + for (size_t i = 0; i < poly_len; i++) { + if (is_buffer) { + px2 = x + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, i * 2)); + py2 = y + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, i * 2 + 1)); + } else { + mp_obj_t *point; + mp_obj_get_array_fixed_n(polygon[i], 2, &point); + px2 = x + mp_obj_get_int(point[0]); + py2 = y + mp_obj_get_int(point[1]); + } + + line(self, px1, py1, px2, py2, col); + px1 = px2; + py1 = py2; + } + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_poly_obj, 5, 5, framebuf_poly); + +STATIC mp_obj_t framebuf_fill_poly(size_t n_args, const mp_obj_t *args) { + (void)n_args; + + mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args[0]); + + mp_int_t x = mp_obj_get_int(args[1]); + mp_int_t y = mp_obj_get_int(args[2]); + + size_t poly_len; + mp_buffer_info_t bufinfo; + mp_obj_t *polygon; + + bool is_buffer = mp_get_buffer(args[3], &bufinfo, MP_BUFFER_READ); + if (is_buffer) { + poly_len = bufinfo.len / (mp_binary_get_size('@', bufinfo.typecode, NULL) * 2); + } else { + mp_obj_get_array(args[3], &poly_len, &polygon); + } + + if (poly_len == 0) { + return mp_const_none; + } + + mp_int_t col = mp_obj_get_int(args[4]); + + mp_int_t px, py; + mp_int_t y_min = INT_MAX, y_max = INT_MIN; + + mp_int_t *points = m_malloc(poly_len * 2 * sizeof(mp_int_t)); + for (size_t i = 0; i < poly_len; i++) { + if (is_buffer) { + px = x + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, i * 2)); + py = y + mp_obj_get_int(mp_binary_get_val_array(bufinfo.typecode, bufinfo.buf, i * 2 + 1)); + } else { + mp_obj_t *point; + mp_obj_get_array_fixed_n(polygon[i], 2, &point); + px = x + mp_obj_get_int(point[0]); + py = y + mp_obj_get_int(point[1]); + } + + points[i * 2] = px; + points[i * 2 + 1] = py; + if (py < y_min) { + y_min = py; + } + if (py > y_max) { + y_max = py; + } + } + + mp_int_t nodes[poly_len]; + for (mp_int_t row = y_min; row <= y_max; row++) { + size_t node_count = 0; + mp_int_t px1 = points[(poly_len - 1) * 2]; + mp_int_t py1 = points[(poly_len - 1) * 2 + 1]; + for (size_t i = 0; i < poly_len; i++) { + mp_int_t px2 = points[i * 2]; + mp_int_t py2 = points[i * 2 + 1]; + + // Find the x-coord of where the polygon side intersects the current row + if (py1 != py2 + && ((py1 > row && py2 <= row) || (py1 <= row && py2 > row))) { + float node = px1 + + (float)(row - py1) / (float)(py2 - py1) + * (px2 - px1); + if (node - (mp_int_t)node <= 0.5f) { + nodes[node_count++] = (mp_int_t)(node); + } else { + nodes[node_count++] = (mp_int_t)(node + 1); + } + } + + px1 = px2; + py1 = py2; + } + + size_t idx = 0; + if (node_count > 0) { + while (idx < node_count - 1) { + if (nodes[idx] > nodes[idx + 1]) { + mp_int_t swap = nodes[idx]; + nodes[idx] = nodes[idx + 1]; + nodes[idx + 1] = swap; + if (idx) { + idx--; + } + } else { + idx++; + } + } + } + + for (size_t i = 0; i < node_count; i += 2) { + fill_rect(self, nodes[i], row, (nodes[i + 1] - nodes[i]) + 1, 1, col); + } + } + + m_free(points + #if MICROPY_MALLOC_USES_ALLOCATED_SIZE + , poly_len * 2 * sizeof(mp_int_t) + #endif + ); + + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_fill_poly_obj, 5, 5, framebuf_fill_poly); + STATIC mp_obj_t framebuf_blit(size_t n_args, const mp_obj_t *args) { mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args[0]); mp_obj_t source_in = mp_obj_cast_to_native_base(args[1], MP_OBJ_FROM_PTR(&mp_type_framebuf)); @@ -610,6 +778,8 @@ STATIC const mp_rom_map_elem_t framebuf_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_vline), MP_ROM_PTR(&framebuf_vline_obj) }, { 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_poly), MP_ROM_PTR(&framebuf_poly_obj) }, + { MP_ROM_QSTR(MP_QSTR_fill_poly), MP_ROM_PTR(&framebuf_fill_poly_obj) }, { 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) }, diff --git a/tests/extmod/framebuf_polygon.py b/tests/extmod/framebuf_polygon.py new file mode 100644 index 0000000000000..6e1323125d7f9 --- /dev/null +++ b/tests/extmod/framebuf_polygon.py @@ -0,0 +1,160 @@ +import sys +from array import array + +try: + import framebuf +except ImportError: + 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") + + +w = 30 +h = 25 +buf = bytearray(w * h) +fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8) +col = 0xFF +col_fill = 0x99 + +# Polygon methods are not present in dynamic natmod context +if not hasattr(fbuf, "poly"): + print("SKIP") + raise SystemExit + +# This describes a arbitrary polygon (this happens to be a concave polygon in the shape of an upper-case letter 'M') +poly = [ + (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) +poly_reversed = [ + (0, 3), + (3, 3), + (6, 10), + (9, 3), + (12, 3), + (12, 20), + (9, 20), + (9, 10), + (6, 17), + (3, 10), + (3, 20), + (0, 20), +] +# This describes the same polygon, but given as a flat array of half words instead of a list of tuples (this is +# more memory efficient way of passing point data) +poly_array = 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, + ], +) + +# Draw the line polygon +fbuf.poly(0, 0, poly, col) + +# Same again, but use a non-zero origin point (i.e., translate the polygon by the given x and y) +# and use the reversed points list +fbuf.poly(15, -2, poly_reversed, col) + +print_buffer(buf, w, h) +fbuf.fill(0) +print() + +# Draw the filled polygon +fbuf.fill_poly(0, 0, poly, col_fill) + +# Same again, but use a non-zero origin point (i.e., translate the polygon by the given x and y) +# and use the reversed points list +fbuf.fill_poly(15, -2, poly_reversed, col_fill) + +print_buffer(buf, w, h) +fbuf.fill(0) +print() + +# Draw line and filled polygons using a flat point array instead of a list of tuples +fbuf.fill_poly(0, 0, poly_array, col_fill) +fbuf.poly(0, 0, poly_array, col) +fbuf.fill_poly(15, -2, poly_array, col_fill) +fbuf.poly(15, -2, poly_array, col) + +print_buffer(buf, w, h) +fbuf.fill(0) +print() + +# Edge cases: These two lists describe self-intersecting polygons +poly_hourglass = [(0, 0), (9, 0), (0, 19), (9, 19)] +poly_star = [(7, 0), (3, 18), (14, 5), (0, 5), (11, 18)] + +fbuf.fill_poly(0, 2, poly_hourglass, col_fill) +fbuf.poly(0, 2, poly_hourglass, col) +fbuf.fill_poly(12, 2, poly_star, col_fill) +fbuf.poly(12, 2, poly_star, col) + +print_buffer(buf, w, h) +fbuf.fill(0) +print() + +# Edge cases: These are "degenerate" polygons +poly_empty = [] # Will draw nothing at all +poly_one = [(20, 20)] # Will draw a single point +poly_two = [(10, 10), (5, 5)] # Will draw a single line +poly_invalid = [(10, 10, 10), (5, 5), (10, 5)] # Error, points must contain 2 coords + +fbuf.poly(0, 0, poly_empty, col) +fbuf.poly(0, 0, poly_one, col) +fbuf.poly(0, 0, poly_two, col) + +try: + fbuf.poly(0, 0, poly_invalid, col) +except ValueError: + print("ValueError") +try: + fbuf.fill_poly(0, 0, poly_invalid, col_fill) +except ValueError: + print("ValueError") + +print_buffer(buf, w, h) +fbuf.fill(0) +print() diff --git a/tests/extmod/framebuf_polygon.py.exp b/tests/extmod/framebuf_polygon.py.exp new file mode 100644 index 0000000000000..826bbc2ee112a --- /dev/null +++ b/tests/extmod/framebuf_polygon.py.exp @@ -0,0 +1,132 @@ + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· + ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· + ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ff ·· ·· ·· ·· ff ·· ff ·· ·· ·· ·· ff ·· ·· + ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ·· ff ·· ·· ff ·· ·· ·· ·· ff ·· ff ·· ·· ·· ·· ff ·· ·· + ff ·· ·· ·· ·· ff ·· ff ·· ·· ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· + ff ·· ·· ·· ·· ff ·· ff ·· ·· ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ff ·· ·· ·· ff ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ff ·· ·· ·· ff ff ·· ·· ff ·· ·· + ff ·· ·· ff ff ·· ·· ·· ff ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ff ·· ff ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ff ·· ·· ·· ff ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ff ·· ff ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ff ·· ff ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ff ·· ff ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· + ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· + ff ·· ·· ff ·· ·· ·· ·· ·· ff ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· 99 99 99 99 99 ·· ·· ·· 99 99 99 99 99 ·· ·· + 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· 99 99 99 99 99 ·· ·· ·· 99 99 99 99 99 ·· ·· + 99 99 99 99 99 ·· ·· ·· 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 ·· 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 ·· ·· ·· 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 ·· 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 ·· 99 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 ·· 99 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· + 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· 99 99 99 99 ·· 99 99 99 ·· 99 99 99 99 ·· ·· + 99 99 99 99 99 99 99 99 99 99 99 99 99 ·· ·· 99 99 99 99 ·· 99 99 99 ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· 99 99 99 ·· 99 99 99 99 ·· ·· 99 99 99 99 ·· ·· 99 ·· ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· 99 99 99 ·· 99 99 99 99 ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· ·· 99 ·· ·· 99 99 99 99 ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· + 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + 99 99 99 99 ·· ·· ·· ·· ·· 99 99 99 99 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· + ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ff 99 99 99 ff ·· ·· ·· ff 99 99 99 ff ·· ·· + ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· ff 99 99 99 ff ·· ·· ·· ff 99 99 99 ff ·· ·· + ff 99 99 99 ff ·· ·· ·· ff 99 99 99 ff ·· ·· ff 99 99 99 99 ff ·· ff 99 99 99 99 ff ·· ·· + ff 99 99 99 ff ·· ·· ·· ff 99 99 99 ff ·· ·· ff 99 99 99 99 ff ·· ff 99 99 99 99 ff ·· ·· + ff 99 99 99 99 ff ·· ff 99 99 99 99 ff ·· ·· ff 99 99 99 99 99 ff 99 99 99 99 99 ff ·· ·· + ff 99 99 99 99 ff ·· ff 99 99 99 99 ff ·· ·· ff 99 99 ff 99 99 ff 99 99 ff 99 99 ff ·· ·· + ff 99 99 99 99 99 ff 99 99 99 99 99 ff ·· ·· ff 99 99 ff 99 99 99 99 99 ff 99 99 ff ·· ·· + ff 99 99 ff 99 99 ff 99 99 ff 99 99 ff ·· ·· ff 99 99 ff ff 99 99 99 ff ff 99 99 ff ·· ·· + ff 99 99 ff 99 99 99 99 99 ff 99 99 ff ·· ·· ff 99 99 ff ff 99 99 99 ff ff 99 99 ff ·· ·· + ff 99 99 ff ff 99 99 99 ff ff 99 99 ff ·· ·· ff 99 99 ff ·· ff 99 ff ·· ff 99 99 ff ·· ·· + ff 99 99 ff ff 99 99 99 ff ff 99 99 ff ·· ·· ff 99 99 ff ·· ff 99 ff ·· ff 99 99 ff ·· ·· + ff 99 99 ff ·· ff 99 ff ·· ff 99 99 ff ·· ·· ff 99 99 ff ·· ·· ff ·· ·· ff 99 99 ff ·· ·· + ff 99 99 ff ·· ff 99 ff ·· ff 99 99 ff ·· ·· ff 99 99 ff ·· ·· ff ·· ·· ff 99 99 ff ·· ·· + ff 99 99 ff ·· ·· ff ·· ·· ff 99 99 ff ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· + ff 99 99 ff ·· ·· ff ·· ·· ff 99 99 ff ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· + ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· + ff 99 99 ff ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ff ff ff ff ·· ·· ·· ·· ·· ff ff ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ff ff ff ff ff ff ff ff ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ff 99 99 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ff 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ff 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ff 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ff 99 99 99 99 ff ·· ·· ·· ·· ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ·· ·· ·· + ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ·· ff 99 99 99 99 ff ·· ff 99 99 99 99 ff ·· ·· ·· ·· + ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ·· ·· ff 99 99 ff ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· + ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ·· ·· ff 99 ff ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ·· ·· ff 99 ff ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ff ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ff 99 ff ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ff 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ff 99 99 ff 99 99 ff ·· ·· ·· ·· ·· ·· ·· + ·· ·· ff 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ff 99 ff ·· ·· ·· ·· ·· ·· ·· + ·· ·· ff 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ·· ·· ff 99 ff ·· ff 99 ff ·· ·· ·· ·· ·· ·· ·· + ·· ff 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ff 99 ff ·· ·· ·· ff 99 ff ·· ·· ·· ·· ·· ·· + ·· ff 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ff ff ·· ·· ·· ·· ·· ·· + ff 99 99 99 99 99 99 99 99 ff ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· + ff ff ff ff ff ff ff ff ff ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + +ValueError +ValueError + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ff ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· + ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· +