From cb4573c4a0965d392fce722f390f891ef1b60f6b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 18 Apr 2024 12:37:33 +0100 Subject: [PATCH] TEST: PicoVector: alright-fonts bringup. --- libraries/pico_vector/af-file-io.h | 16 + libraries/pico_vector/af-memory.h | 5 + libraries/pico_vector/alright-fonts.h | 361 ++++++++++++++++++ libraries/pico_vector/pico_vector.cpp | 22 +- libraries/pico_vector/pico_vector.hpp | 29 +- micropython/modules/picovector/picovector.cpp | 78 ++-- 6 files changed, 468 insertions(+), 43 deletions(-) create mode 100644 libraries/pico_vector/af-file-io.h create mode 100644 libraries/pico_vector/af-memory.h create mode 100644 libraries/pico_vector/alright-fonts.h diff --git a/libraries/pico_vector/af-file-io.h b/libraries/pico_vector/af-file-io.h new file mode 100644 index 000000000..eb5a413b6 --- /dev/null +++ b/libraries/pico_vector/af-file-io.h @@ -0,0 +1,16 @@ +#include +#include + +extern "C" { +void* fileio_open(const char* filename); + +void fileio_close(void* fhandle); + +size_t fileio_read(void* fhandle, void *buf, size_t len); + +int fileio_getc(void* fhandle); + +size_t fileio_tell(void* fhandle); + +size_t fileio_seek(void* fhandle, size_t pos); +} \ No newline at end of file diff --git a/libraries/pico_vector/af-memory.h b/libraries/pico_vector/af-memory.h new file mode 100644 index 000000000..81c647bda --- /dev/null +++ b/libraries/pico_vector/af-memory.h @@ -0,0 +1,5 @@ +extern "C" { + void *af_malloc(size_t size); + void *af_realloc(void *p, size_t size); + void af_free(void *p); +} \ No newline at end of file diff --git a/libraries/pico_vector/alright-fonts.h b/libraries/pico_vector/alright-fonts.h new file mode 100644 index 000000000..c8782bdfb --- /dev/null +++ b/libraries/pico_vector/alright-fonts.h @@ -0,0 +1,361 @@ +/* + + Alright Fonts 🖍 - a font format for embedded and low resource platforms. + + Jonathan Williamson, August 2022 + Examples, source, and more: https://github.com/lowfatcode/pretty-poly + MIT License https://github.com/lowfatcode/pretty-poly/blob/main/LICENSE + + An easy way to render high quality text in embedded applications running + on resource constrained microcontrollers such as the Cortex M0 and up. + + - OTF and TTF support: generate efficient packed fonts easily + - Minimal data: ~4kB (40 bytes per char) for printable ASCII set (Roboto) + - Tunable: trade off file size, contour complexity, and visual quality + - Metrics: advance and bounding box for fast layout + - UTF-8 or ASCII: support for non ASCII like Kanji or Cyrillic + - Fixed scale: coords scaled to ^2 bounds for fast scaling (no divide) + - C17 header only library: simply copy the header file into your project + - Customised font packs: include only the characters you need + - Simple outlines: all paths are simply polylines for easy rendering + - Easy antialiasing: combine with Pretty Poly for quick results! + +*/ + +#ifndef AF_INCLUDE_H +#define AF_INCLUDE_H + +#include +#include +#include +#include +#include +#include + +#ifdef AF_MALLOC + #ifndef PP_MALLOC + #define PP_MALLOC(size) AF_MALLOC(size) + #define PP_REALLOC(p, size) AF_REALLOC(p, size) + #define PP_FREE(p) AF_FREE(p) + #endif // PP_MALLOC +#endif // AF_MALLOC + +#ifndef AF_MALLOC + #define AF_MALLOC(size) malloc(size) + #define AF_REALLOC(p, size) realloc(p, size) + #define AF_FREE(p) free(p) +#endif // AF_MALLOC + +#ifndef AF_FILE + #define AF_FILE FILE* + #define AF_FREAD(p, size, nmemb, stream) fread(p, size, nmemb, stream) + #define AF_FGETC(stream) fgetc(stream) +#endif + +#include "pretty-poly.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + int8_t x, y; +} af_point_t; +pp_point_t af_point_transform(pp_point_t *p, pp_mat3_t *m); + +typedef struct { + uint8_t point_count; + af_point_t *points; +} af_path_t; + +typedef struct { + char codepoint; + int8_t x, y, w, h; + int8_t advance; + uint8_t path_count; + af_path_t *paths; +} af_glyph_t; + +typedef struct { + uint16_t flags; + uint16_t glyph_count; + af_glyph_t *glyphs; +} af_face_t; + +typedef enum { + AF_H_ALIGN_LEFT = 0, AF_H_ALIGN_CENTER = 1, AF_H_ALIGN_RIGHT = 2, + AF_H_ALIGN_JUSTIFY = 4, + AF_V_ALIGN_TOP = 8, AF_V_ALIGN_MIDDLE = 16, AF_V_ALIGN_BOTTOM = 32 +} af_align_t; + +typedef struct { + af_face_t *face; // font + float size; // text size in pixels + float line_height; // spacing between lines (%) + float letter_spacing; // spacing between characters (%) + float word_spacing; // spacing between words (%) + af_align_t align; // horizontal and vertical alignment + pp_mat3_t *transform; // arbitrary transformation +} af_text_metrics_t; + +bool af_load_font_file(AF_FILE file, af_face_t *face); +void af_render_character(af_face_t *face, wchar_t codepoint, af_text_metrics_t *tm); +void af_render(af_face_t *face, wchar_t *text, af_text_metrics_t *tm); +pp_rect_t af_measure(af_face_t *face, const wchar_t *text, af_text_metrics_t *tm); + +#ifdef AF_USE_PRETTY_POLY +#endif + +#ifdef __cplusplus +} +#endif + +#ifdef AF_IMPLEMENTATION + + +/* + helper functions +*/ + +// big endian file reading helpers +uint16_t ru16(AF_FILE file) {uint8_t w[2]; AF_FREAD((char *) w, 1, 2, file); return w[0] << 8 | w[1];} +int16_t rs16(AF_FILE file) {uint8_t w[2]; AF_FREAD((char *) w, 1, 2, file); return w[0] << 8 | w[1];} +uint32_t ru32(AF_FILE file) {uint8_t dw[4]; AF_FREAD((char *)dw, 1, 4, file); return dw[0] << 24 | dw[1] << 16 | dw[2] << 8 | dw[3];} +uint8_t ru8(AF_FILE file) {return AF_FGETC(file);} +int8_t rs8(AF_FILE file) {return AF_FGETC(file);} + +bool af_load_font_file(AF_FILE file, af_face_t *face) { + // check header magic bytes are present + char marker[4]; AF_FREAD(marker, 1, 4, file); + if(memcmp(marker, "af!?", 4) != 0) { + return false; // doesn't start with magic marker + } + + // extract flags and ensure none set + face->flags = ru16(file); + if(face->flags != 0) { + return false; // unknown flags set + } + + // number of glyphs, paths, and points in font + uint16_t glyph_count = ru16(file); + uint16_t path_count = ru16(file); + uint16_t point_count = ru16(file); + + // allocate buffer to store font glyph, path, and point data + void *buffer = AF_MALLOC(sizeof(af_glyph_t) * glyph_count + \ + sizeof( af_path_t) * path_count + \ + sizeof(af_point_t) * point_count); + af_glyph_t *glyphs = (af_glyph_t *) buffer; + af_path_t *paths = ( af_path_t *)(glyphs + (sizeof(af_glyph_t) * glyph_count)); + af_point_t *points = (af_point_t *)( paths + (sizeof( af_path_t) * path_count)); + + // load glyph dictionary + face->glyph_count = glyph_count; + face->glyphs = glyphs; + for(int i = 0; i < glyph_count; i++) { + af_glyph_t *glyph = &face->glyphs[i]; + glyph->codepoint = ru16(file); + glyph->x = rs8(file); + glyph->y = rs8(file); + glyph->w = ru8(file); + glyph->h = ru8(file); + glyph->advance = ru8(file); + glyph->path_count = ru8(file); + glyph->paths = paths; + paths += sizeof(af_path_t) * glyph->path_count; + } + + // load the glyph paths + for(int i = 0; i < glyph_count; i++) { + af_glyph_t *glyph = &face->glyphs[i]; + for(int j = 0; j < glyph->path_count; j++) { + af_path_t *path = &glyph->paths[j]; + path->point_count = ru8(file); + path->points = points; + points += sizeof(af_point_t) * path->point_count; + } + } + + // load the glyph points + for(int i = 0; i < glyph_count; i++) { + af_glyph_t *glyph = &face->glyphs[i]; + for(int j = 0; j < glyph->path_count; j++) { + af_path_t *path = &glyph->paths[j]; + for(int k = 0; k < path->point_count; k++) { + af_point_t *point = &path->points[k]; + point->x = ru8(file); + point->y = ru8(file); + } + } + } + + return true; +} + +af_glyph_t *find_glyph(af_face_t *face, wchar_t c) { + for(int i = 0; i < face->glyph_count; i++) { + if(face->glyphs[i].codepoint == c) { + return &face->glyphs[i]; + } + } + return NULL; +} + +void af_render_glyph(af_glyph_t* glyph, af_text_metrics_t *tm) { + assert(glyph != NULL); + + pp_poly_t poly; + poly.count = glyph->path_count; + poly.paths = (pp_path_t *)AF_MALLOC(poly.count * sizeof(pp_path_t)); + for(uint32_t i = 0; i < poly.count; i++) { + pp_path_t *path = &poly.paths[i]; + path->count = glyph->paths[i].point_count; + path->points = (pp_point_t *)AF_MALLOC(glyph->paths[i].point_count * sizeof(pp_point_t)); + for(uint32_t j = 0; j < path->count; j++) { + pp_point_t *point = &path->points[j]; + point->x = glyph->paths[i].points[j].x; + point->y = glyph->paths[i].points[j].y; + } + } + + pp_render(&poly); + + for(uint32_t i = 0; i < poly.count; i++) { + pp_path_t *path = &poly.paths[i]; + free(path->points); + } + free(poly.paths); +} + +void af_render_character(af_face_t *face, wchar_t c, af_text_metrics_t *tm) { + af_glyph_t *glyph = find_glyph(face, c); + if(!glyph) { + return; + } + af_render_glyph(glyph, tm); +} + +int get_line_width(af_face_t *face, wchar_t *text, af_text_metrics_t *tm) { + int line_width = 0; + wchar_t *end = wcschr(text, L'\n'); + for(wchar_t c = *text; text < end; text++, c = *text) { + af_glyph_t *glyph = find_glyph(face, c); + if(!glyph) { + continue; + } + + if(c == L' ') { + line_width += (glyph->advance * tm->word_spacing) / 100.0f; + } else { + line_width += (glyph->advance * tm->letter_spacing) / 100.0f; + } + } + return line_width; +} + +int get_max_line_width(af_face_t *face, wchar_t *text, af_text_metrics_t *tm) { + int max_width = 0; + + wchar_t *end = wcschr(text, L'\n'); + while(end) { + int width = get_line_width(face, text, tm); + max_width = max_width < width ? width : max_width; + text = end + 1; + end = wcschr(text, L'\n'); + } + + return max_width; +} + + +void af_render(af_face_t *face, wchar_t *text, af_text_metrics_t *tm) { + pp_mat3_t *old = pp_transform(NULL); + + float line_height = (tm->line_height * 128.0f) / 100.0f; + float scale = tm->size / 128.0f; + + // find maximum line length + int max_line_width = get_max_line_width(face, text, tm); + + struct { + float x, y; + } caret; + + caret.x = 0; + caret.y = 0; + + wchar_t *end = wcschr(text, L'\n'); + while(end) { + int line_width = get_line_width(face, text, tm); + + for(wchar_t c = *text; text < end; text++, c = *text) { + af_glyph_t *glyph = find_glyph(face, c); + if(!glyph) { + continue; + } + + pp_mat3_t caret_transform = *tm->transform; + pp_mat3_scale(&caret_transform, scale, scale); + pp_mat3_translate(&caret_transform, caret.x, caret.y); + + if(tm->align == AF_H_ALIGN_CENTER) { + pp_mat3_translate(&caret_transform, (max_line_width - line_width) / 2, 0); + } + + if(tm->align == AF_H_ALIGN_RIGHT) { + pp_mat3_translate(&caret_transform, (max_line_width - line_width), 0); + } + + pp_transform(&caret_transform); + + af_render_glyph(glyph, tm); + + if(c == L' ') { + caret.x += (glyph->advance * tm->word_spacing) / 100.0f; + } else { + caret.x += (glyph->advance * tm->letter_spacing) / 100.0f; + } + + } + + text = end + 1; + end = wcschr(text, L'\n'); + + caret.x = 0; + caret.y += line_height; + } + + + + pp_transform(old); +} + +pp_rect_t af_measure(af_face_t *face, const wchar_t *text, af_text_metrics_t *tm) { + pp_rect_t result; + bool first = true; + pp_mat3_t t = *tm->transform; + + for(size_t i = 0; i < wcslen(text); i++) { + af_glyph_t *glyph = find_glyph(face, text[i]); + if(!glyph) { + continue; + } + pp_rect_t r = {glyph->x, glyph->y, glyph->x + glyph->w, glyph->y + glyph->h}; + r = pp_rect_transform(&r, &t); + pp_mat3_translate(&t, glyph->advance, 0); + + if(first) { + result = r; + first = false; + }else{ + result = pp_rect_merge(&result, &r); + } + } + + return result; +} + +#endif // AF_IMPLEMENTATION + +#endif // AF_INCLUDE_H \ No newline at end of file diff --git a/libraries/pico_vector/pico_vector.cpp b/libraries/pico_vector/pico_vector.cpp index 25d533b9f..8566e43fb 100644 --- a/libraries/pico_vector/pico_vector.cpp +++ b/libraries/pico_vector/pico_vector.cpp @@ -1,4 +1,5 @@ #define PP_IMPLEMENTATION +#define AF_IMPLEMENTATION #include "pico_vector.hpp" #include @@ -55,7 +56,7 @@ namespace pimoroni { } } - pp_point_t PicoVector::text(std::string_view text, pp_point_t offset, pp_mat3_t *t) { + pp_point_t PicoVector::text(std::wstring_view text, pp_point_t offset, pp_mat3_t *t) { pp_point_t caret = {0, 0}; // Align text from the bottom left @@ -68,7 +69,9 @@ namespace pimoroni { pp_point_t space; pp_point_t carriage_return = {0, -(PP_COORD_TYPE)text_metrics.line_height}; - space.x = alright_fonts::measure_character(text_metrics, ' ').w; + wchar_t spc = L' '; + + space.x = af_measure(text_metrics.face, &spc, &text_metrics).w; if (space.x == 0) { space.x = text_metrics.word_spacing; } @@ -97,7 +100,7 @@ namespace pimoroni { uint16_t word_width = 0; for(size_t j = i; j < next_break; j++) { - word_width += alright_fonts::measure_character(text_metrics, text[j]).w; + word_width += af_measure(text_metrics.face, &text[j], &text_metrics).w; word_width += text_metrics.letter_spacing; } @@ -107,17 +110,22 @@ namespace pimoroni { } for(size_t j = i; j < std::min(next_break + 1, text.length()); j++) { - if (text[j] == '\n') { // Linebreak + if (text[j] == L'\n') { // Linebreak caret = pp_point_sub(&caret, &carriage_return); carriage_return = initial_carriage_return; - } else if (text[j] == ' ') { // Space + } else if (text[j] == L' ') { // Space caret = pp_point_add(&caret, &space); carriage_return = pp_point_add(&carriage_return, &space); } else { - alright_fonts::render_character(text_metrics, text[j], caret, t); + // apply the caret offset... + pp_mat3_t pos = pp_mat3_identity(); + pp_mat3_mul(&pos, t); + pp_mat3_translate(&pos, caret.x, caret.y); + text_metrics.transform = &pos; + af_render_character(text_metrics.face, text[j], &text_metrics); } pp_point_t advance = { - (PP_COORD_TYPE)alright_fonts::measure_character(text_metrics, text[j]).w + text_metrics.letter_spacing, + (PP_COORD_TYPE)af_measure(text_metrics.face, (const wchar_t *)text[j], &text_metrics).w + text_metrics.letter_spacing, (PP_COORD_TYPE)0 }; advance = pp_point_transform(&advance, t); diff --git a/libraries/pico_vector/pico_vector.hpp b/libraries/pico_vector/pico_vector.hpp index 86fc2671d..fd1532b0f 100644 --- a/libraries/pico_vector/pico_vector.hpp +++ b/libraries/pico_vector/pico_vector.hpp @@ -1,5 +1,21 @@ + +#include "af-file-io.h" +#include "af-memory.h" + +#define AF_FILE void* +#define AF_FREAD(p, size, nmemb, stream) fileio_read(stream, p, nmemb) +#define AF_FGETC(stream) fileio_getc(stream) + +#define AF_MALLOC(size) af_malloc(size) +#define AF_REALLOC(p, size) af_realloc(p, size) +#define AF_FREE(p) af_free(p) + +#define PP_MALLOC(size) af_malloc(size) +#define PP_REALLOC(p, size) af_realloc(p, size) +#define PP_FREE(p) af_free(p) + #include "pretty-poly.h" -#include "alright_fonts.hpp" +#include "alright-fonts.h" #include "pico_graphics.hpp" pp_rect_t pp_contour_bounds(const pp_path_t *c); @@ -12,7 +28,7 @@ namespace pimoroni { class PicoVector { private: static PicoGraphics *graphics; - alright_fonts::text_metrics_t text_metrics; + af_text_metrics_t text_metrics; static constexpr uint8_t alpha_map[4] {0, 128, 192, 255}; public: @@ -66,18 +82,21 @@ namespace pimoroni { } void set_font_size(unsigned int font_size) { - text_metrics.set_size(font_size); + text_metrics.size = font_size; } bool set_font(std::string_view font_path, unsigned int font_size) { - bool result = text_metrics.face.load(font_path); + //bool result = text_metrics.face.load(font_path); + void* font = fileio_open(font_path.data()); + af_load_font_file(font, text_metrics.face); + bool result = false; set_font_size(font_size); return result; } - pp_point_t text(std::string_view text, pp_point_t origin, pp_mat3_t *t); + pp_point_t text(std::wstring_view text, pp_point_t origin, pp_mat3_t *t); void transform(pp_path_t *path, pp_mat3_t *t); void transform(pp_poly_t *poly, pp_mat3_t *t); diff --git a/micropython/modules/picovector/picovector.cpp b/micropython/modules/picovector/picovector.cpp index fa4b9303c..4cad8ad49 100644 --- a/micropython/modules/picovector/picovector.cpp +++ b/micropython/modules/picovector/picovector.cpp @@ -29,10 +29,24 @@ typedef struct _PATH_obj_t { pp_path_t path; } _PATH_obj_t; -file_io::file_io(std::string_view filename) { - mp_obj_t fn = mp_obj_new_str(filename.data(), (mp_uint_t)filename.size()); +void *af_malloc(size_t size) { + mp_printf(&mp_plat_print, "af_malloc %lu\n", size); + return m_tracked_calloc(sizeof(uint8_t), size); +} + +void *af_realloc(void *p, size_t size) { + return NULL; +} - //mp_printf(&mp_plat_print, "Opening file %s\n", filename.data()); +void af_free(void *p) { + mp_printf(&mp_plat_print, "af_free\n"); + m_tracked_free(p); +} + +void* fileio_open(const char *filename) { + mp_obj_t fn = mp_obj_new_str(filename, (mp_uint_t)strlen(filename)); + + mp_printf(&mp_plat_print, "Opening file %s\n", filename); mp_obj_t args[2] = { fn, @@ -43,34 +57,39 @@ file_io::file_io(std::string_view filename) { // example tuple response: (32768, 0, 0, 0, 0, 0, 5153, 1654709815, 1654709815, 1654709815) mp_obj_t stat = mp_vfs_stat(fn); mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR2(stat, mp_obj_tuple_t); - filesize = mp_obj_get_int(tuple->items[6]); + int filesize = mp_obj_get_int(tuple->items[6]); + mp_printf(&mp_plat_print, "Size %lu\n", filesize); mp_obj_t fhandle = mp_vfs_open(MP_ARRAY_SIZE(args), &args[0], (mp_map_t *)&mp_const_empty_map); - this->state = (void *)fhandle; + return (void*)fhandle; } -file_io::~file_io() { - mp_stream_close((mp_obj_t)this->state); +void fileio_close(void* fhandle) { + mp_stream_close((mp_obj_t)fhandle); } -size_t file_io::read(void *buf, size_t len) { - //mp_printf(&mp_plat_print, "Reading %lu bytes\n", len); - mp_obj_t fhandle = this->state; +size_t fileio_read(void* fhandle, void *buf, size_t len) { + mp_printf(&mp_plat_print, "Reading %lu bytes\n", len); int error; - return mp_stream_read_exactly(fhandle, buf, len, &error); + return mp_stream_read_exactly((mp_obj_t)fhandle, buf, len, &error); } -size_t file_io::tell() { - mp_obj_t fhandle = this->state; +int fileio_getc(void* fhandle) { + unsigned char buf; + fileio_read((mp_obj_t)fhandle, &buf, 1); + return (int)buf; +} + +size_t fileio_tell(void* fhandle) { struct mp_stream_seek_t seek_s; seek_s.offset = 0; seek_s.whence = SEEK_CUR; - const mp_stream_p_t *stream_p = mp_get_stream(fhandle); + const mp_stream_p_t *stream_p = mp_get_stream((mp_obj_t)fhandle); int error; - mp_uint_t res = stream_p->ioctl(fhandle, MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &error); + mp_uint_t res = stream_p->ioctl((mp_obj_t)fhandle, MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &error); if (res == MP_STREAM_ERROR) { mp_raise_OSError(error); } @@ -78,21 +97,16 @@ size_t file_io::tell() { return seek_s.offset; } -bool file_io::fail() { - return false; -} - // Re-implementation of stream.c/STATIC mp_obj_t stream_seek(size_t n_args, const mp_obj_t *args) -size_t file_io::seek(size_t pos) { - mp_obj_t fhandle = this->state; +size_t fileio_seek(void* fhandle, size_t pos) { struct mp_stream_seek_t seek_s; seek_s.offset = pos; seek_s.whence = SEEK_SET; - const mp_stream_p_t *stream_p = mp_get_stream(fhandle); + const mp_stream_p_t *stream_p = mp_get_stream((mp_obj_t)fhandle); int error; - mp_uint_t res = stream_p->ioctl(fhandle, MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &error); + mp_uint_t res = stream_p->ioctl((mp_obj_t)fhandle, MP_STREAM_SEEK, (mp_uint_t)(uintptr_t)&seek_s, &error); if (res == MP_STREAM_ERROR) { mp_raise_OSError(error); } @@ -347,7 +361,8 @@ mp_obj_t VECTOR_set_font(mp_obj_t self_in, mp_obj_t font, mp_obj_t size) { if (mp_obj_is_str(font)) { // TODO: Implement when Alright Fonts rewrite is ready - //result = self->vector->set_font(mp_obj_to_string_r(font), font_size); + GET_STR_DATA_LEN(font, str, str_len); + result = self->vector->set_font((const char*)str, font_size); } else { @@ -362,7 +377,7 @@ mp_obj_t VECTOR_set_font_size(mp_obj_t self_in, mp_obj_t size) { int font_size = mp_obj_get_int(size); (void)font_size; // TODO: Implement when Alright Fonts rewrite is ready - //self->vector->set_font_size(font_size); + self->vector->set_font_size(font_size); return mp_const_none; } @@ -395,20 +410,21 @@ mp_obj_t VECTOR_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) GET_STR_DATA_LEN(text_obj, str, str_len); - const std::string_view t((const char*)str, str_len); + const std::wstring_view t((const wchar_t *)str, str_len); int x = args[ARG_x].u_int; int y = args[ARG_y].u_int; (void)x; (void)y; - if(args[ARG_angle].u_obj == mp_const_none) { - // TODO: Implement when Alright Fonts rewrite is ready - //self->vector->text(t, Point(x, y)); - } else { - //self->vector->text(t, Point(x, y), mp_obj_get_float(args[ARG_angle].u_obj)); + pp_mat3_t tt = pp_mat3_identity(); + + if(args[ARG_angle].u_obj != mp_const_none) { + pp_mat3_rotate(&tt, mp_obj_get_float(args[ARG_angle].u_obj)); } + self->vector->text(t, {(float)x, (float)y}, &tt); + return mp_const_none; }