diff --git a/.cursor/frozen-self-installer-notes.md b/.cursor/frozen-self-installer-notes.md index 5d780139d..c4a84947a 100644 --- a/.cursor/frozen-self-installer-notes.md +++ b/.cursor/frozen-self-installer-notes.md @@ -6,7 +6,7 @@ Private design scratchpad (not for RTD). - [ ] Warn users where downloads come from: PyDevices micropython-lib MIP index (`https://PyDevices.github.io/micropython-lib/mip/PyDevices`), not the [official MicroPython micropython-lib](https://github.com/micropython/micropython-lib) package index — **maintainer-published**, not an endorsed upstream source (show URL on first run / in GUI) - [ ] Freeze a small bootstrap module into desktop MicroPython builds so `from import ` works out of the box -- [ ] Install or refresh all 4 core modules (`displaysys`, `eventsys`, `graphics`, `multimer`) via `mip` / `lib_install`-style fetch (skip re-download when up to date?) +- [ ] Install or refresh all 3 core modules (`displaysys`, `eventsys`, `graphics`, `multimer`) via `mip` / `lib_install`-style fetch (skip re-download when up to date?) - [ ] Post-install GUI (TBD: terminal menu vs minimal on-display UI): download more files, system/platform info, `lv_test_timer_*`-style sanity checks, link to `spotapi_remote` / spotapi - [ ] Flesh out scope, module name, and UX (name the import, entry points, error handling offline) diff --git a/.cursor/rules/graphics-parity-and-timer-async.mdc b/.cursor/rules/graphics-parity-and-timer-async.mdc index 441ff7487..90109caf4 100644 --- a/.cursor/rules/graphics-parity-and-timer-async.mdc +++ b/.cursor/rules/graphics-parity-and-timer-async.mdc @@ -7,11 +7,11 @@ alwaysApply: true Active objectives for pydisplay + `cmods/graphics` work. Complements global `fix-not-workaround` rule. -## Graphics-cmod ↔ `src/lib/graphics` +## Graphics-cmod ↔ sibling `graphics/lib/graphics` -- **Goal:** 100% behavioral parity with `src/lib/graphics` (API, kwargs, duck typing, BMP565, Draw, Font, file I/O). +- **Goal:** 100% behavioral parity with [PyDevices/graphics](https://github.com/PyDevices/graphics) `lib/graphics/` (API, kwargs, duck typing, BMP565, Draw, Font, file I/O). - When a difference is found (matrix failure, code review, or side-by-side with Python), **fix it in the cmod immediately** — do not defer or patch callers in pydisplay examples. -- Reference implementation is Python under `src/lib/graphics/`; cmod must match the contract, not a subset. +- Reference implementation is the pure-Python package under `graphics/lib/graphics/`; cmod must match the contract, not a subset. - MP and CP share bindings (`gfx_bindings_mp.c`, `gfx_module_mp.c`); fix both runtimes when the gap is in shared code. - Rebuild MP/CP after cmod changes; re-run affected examples before moving on. diff --git a/.cursor/testpypi-naming-convention.md b/.cursor/testpypi-naming-convention.md index c46f128d9..fff056a62 100644 --- a/.cursor/testpypi-naming-convention.md +++ b/.cursor/testpypi-naming-convention.md @@ -5,7 +5,7 @@ PyDevices publishes **CPython wheels to TestPyPI only** (not production PyPI). E | Role | Example | Used by | |------|---------|---------| | **MIP / micropython-lib** | `graphics` | `mip.install("graphics", index=…)`, manifest `package("graphics")` | -| **pip / TestPyPI project** | `pydisplay-graphics` | `pip install … pydisplay-graphics`, hatch `[project].name` | +| **pip / TestPyPI project** | `graphics-py` | `pip install … graphics-py`, hatch `[project].name` | | **Python import** | `graphics` | `import graphics` in application code | MIP names stay short. pip project names must **not collide with [pypi.org](https://pypi.org)** — TestPyPI rejects sdists when the normalized name is already registered there (wheels may still upload; treat collisions as errors). @@ -29,7 +29,7 @@ When the MIP name is **taken on pypi.org**, prefix with `pydisplay-`: | MIP name | pip / TestPyPI | Import | Why | |----------|----------------|--------|-----| -| `graphics` | **`pydisplay-graphics`** | `graphics` | [pypi.org/project/graphics](https://pypi.org/project/graphics) exists | +| `graphics` | **`graphics-py`** | `graphics` | [pypi.org/project/graphics](https://pypi.org/project/graphics) exists | Mapping lives in `pypi_publish_name()` in [`scripts/publish_sync_packages.sh`](../scripts/publish_sync_packages.sh). MIP and source trees keep the short name `graphics/`. @@ -44,7 +44,7 @@ Use a **repo-specific suffix** so pip names are unique and intent is obvious: Do **not** publish as bare `lvgl` — [pypi.org/project/lvgl](https://pypi.org/project/lvgl) exists. -`graphics-cmod` and `pydisplay-graphics` both provide `import graphics`; prefer **`graphics-cmod`** on desktop/Android when the native wheel matches the platform, and **`pydisplay-graphics`** for pure-Python-only or cross-check installs. +`graphics-cmod` and `graphics-py` (both from [PyDevices/graphics](https://github.com/PyDevices/graphics)) provide `import graphics`; prefer **`graphics-cmod`** on desktop/Android when the native wheel matches the platform, and **`graphics-py`** for pure-Python-only or cross-check installs. ### 4. displaysys is one package @@ -82,7 +82,7 @@ After a version is on TestPyPI, **do not rename** the project (TestPyPI rejects | `displaysys` | `displaysys` | `displaysys`, `board_config` | free | | `eventsys` | `eventsys` | `eventsys` | free | | `multimer` | `multimer` | `multimer` | free | -| `graphics` | **`pydisplay-graphics`** | `graphics` | **taken** → mapped | +| `graphics` | **`graphics-py`** | `graphics` | **taken** → mapped | ### Sibling repos (own workflows) diff --git a/.cursor/testpypi-publish-audit.md b/.cursor/testpypi-publish-audit.md index 1ee7e8900..3857ec736 100644 --- a/.cursor/testpypi-publish-audit.md +++ b/.cursor/testpypi-publish-audit.md @@ -28,7 +28,7 @@ Audited **2026-07-08** from local clones under `~/github/cmods` and `~/github/py | `displaysys` | `displaysys-0.0.7-py2.py3-none-any.whl` | universal; full tree + `board_config.py` | | `eventsys` | `eventsys-0.0.7-py2.py3-none-any.whl` | universal | | `multimer` | `multimer-0.0.7-py2.py3-none-any.whl` | universal | -| `pydisplay-graphics` | `pydisplay_graphics-0.0.7-py2.py3-none-any.whl` | universal (PyPI name mapped from `graphics`) | +| `graphics-py` | `pydisplay_graphics-0.0.7-py2.py3-none-any.whl` | universal (PyPI name mapped from `graphics`) | **Layout:** `displaysys` is the full package (all modules under `src/lib/displaysys/` plus `board_config.py`). Per-backend `displaysys-*` packages are **not** published. Published packages do not include `examples/` trees. @@ -57,9 +57,12 @@ Both use the same shape: matrix `ubuntu-latest` + `windows-latest`, plus a dedic ## usdl2 -- **Workflow:** `usdl2/.github/workflows/publish-testpypi.yml` -- **Package:** pure-Python ctypes shim (`py3-none-any`); native code is the **MicroPython user C module** built into firmware, not a CPython wheel -- **Linux / Windows / Android:** one universal wheel is intentional — Android/desktop load `libSDL2.so` / `SDL2.dll` at runtime via ctypes +- **Workflows:** `usdl2/.github/workflows/publish-testpypi.yml` (native cibuildwheel); + `publish-micropython-lib.yml` (TestPyPI `usdl2-py` + MIP) +- **Native package:** platform wheels (`manylinux`, `win_amd64`, `android_21_*`) — + CPython extension + MicroPython/CircuitPython usermod +- **Pure Python:** `usdl2-py` / MIP `usdl2` from `lib/usdl2.py` (ctypes/ffi fallback); + same `vX.Y.Z` as native - **Tags on GitHub:** semver release tags (`v*.*.*`) exist; **no GitHub Release** objects ## Repos without TestPyPI automation @@ -81,7 +84,7 @@ Both use the same shape: matrix `ubuntu-latest` + `windows-latest`, plus a dedic | Category | Status | |----------|--------| | **Native CPython extensions** (`lvgl-cpython`, `graphics-cmod`) | **Met** — CI builds linux + windows + android wheels | -| **Pure pydisplay libs** (`displaysys`, `eventsys`, `multimer`, `pydisplay-graphics`) | **Met by design** — universal wheels; manifest `require()` graph in § Pip dependency graph | +| **Pure pydisplay libs** (`displaysys`, `eventsys`, `multimer`, `graphics-py`) | **Met by design** — universal wheels; manifest `require()` graph in § Pip dependency graph | | **usdl2** | **Met for CPython shim** — universal wheel; MP cmod is separate | | **displayif** | **N/A** — firmware-only user C module, not a pip/MIP package | | **displaysys-* backends** | **Removed** — use full `displaysys` only | diff --git a/.gitignore b/.gitignore index 4c343e248..f3185ea0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ src/add_ons/gui -# Generated from src/add_ons/framebuf.py (scripts/install_sync_framebuf.py; publish + tests) -src/lib/graphics/framebuf.py # Optional local/dev copy of LVGL wasm wheels (not required; pyodide uses TestPyPI) web/pyscript/wheels @@ -216,3 +214,7 @@ cython_debug/ .ruff_cache/ + +# CI / local sibling checkouts inside this workspace +graphics/ +micropython-hardware/ diff --git a/AGENTS.md b/AGENTS.md index 654de5cea..a345a0e8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,16 +67,15 @@ is a symlink to `../../src`, so editing `src/` updates the PyScript gallery too. - Known pre-existing example failures on CPython (not environment issues to "fix"): `nano_gui_simpletest` needs the matching Hinch `gui/` package. `tools/png_test.py` in **pdwidgets** (PNG probe) needs `PDWIDGETS_PNG_DIR` / material-design-icons and a sibling pydisplay checkout. -- **`palettes` / `pdwidgets` sibling repos** (`hello`, `color_test`, `feathers`, - `graphics_simpletest`, `palettes_demo`, `widgets_*`, … import `palettes` and/or - `pdwidgets`). These are source-only PyDevices repos, **not** pip packages — the - PyPI project literally named `palettes` is an unrelated "random hex color" - library, so do **not** `pip install palettes`. They normally arrive as - repositoryDependencies under `/agent/repos/{palettes,pdwidgets}` (symlinked into - `~/gh/pydevices/`); if that clone is missing, clone - `github.com/PyDevices/{palettes,pdwidgets}` into a writable dir and put their - `src` dirs on the venv path (e.g. a `*.pth` in `.venv/lib/*/site-packages` - listing `/palettes/src` and `/pdwidgets/src`, or `PYTHONPATH`). +- **Sibling pure-Python repos** (`palettes`, `pdwidgets`, `graphics`, `usdl2` + under `lib/`). Examples that `import palettes` / `pdwidgets` / `graphics` / + the ctypes `usdl2` fallback need those `lib/` dirs on path. The PyPI project + literally named `palettes` is an unrelated "random hex color" library — do + **not** `pip install palettes`. Prefer native TestPyPI wheels for `graphics` + (`graphics-cmod`) and `usdl2` when available. Clones normally live under + `/agent/repos/{…}` (symlinked into `~/gh/pydevices/`); if missing, clone + `github.com/PyDevices/` and put `/lib` on the venv path + (`.pth` / `PYTHONPATH`). Quick setup: `bash scripts/setup_sibling_repos.sh` (clones current `main` and writes `.pth` files). The example harness (`tools/sibling_repos.py`) auto-discovers the same paths for matrix runs. diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md index 1c9e32c1f..ba848cadd 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -45,7 +45,7 @@ flowchart TB | **`eventsys`** | `Runtime` pumps input and dispatches PyGame/SDL2-style events to callbacks; prefer `runtime.on(...)` + `runtime.run_forever()`. | | **`graphics`** | Optional helpers on top of `framebuf` (rounded rects, gradients, `Area` bounding boxes). | | **`multimer`** | Cross-platform `Timer` / `AsyncTimer`, ticks/sleep, and `asyncio` exposure. | -| **`add_ons`** | Optional shims and integrations (`framebuf` on CPython, `displaybuf`, `pdwidgets`, config templates). | +| **`add_ons`** | Optional shims and integrations (`displaybuf`, config templates, GUI fetch helpers). | ## Typical boot sequence diff --git a/docs/concepts/displays.md b/docs/concepts/displays.md index 9c8979fe7..58cffc549 100644 --- a/docs/concepts/displays.md +++ b/docs/concepts/displays.md @@ -30,7 +30,7 @@ For microcontrollers on **MicroPython and CircuitPython**. SDL2 desktop backend (CPython, MicroPython Unix, CircuitPython Unix). Uses an SDL texture as GRAM. It is the default on MicroPython Unix and available on CPython via `board_configs/sdldisplay/`. -SDL2 bindings for **`SDLDisplay`**: native frozen **`usdl2`** when built in, else **`add_ons/usdl2.py`** on ``sys.path``. Build **`usdl2`** from [PyDevices/usdl2](https://github.com/PyDevices/usdl2) for frozen MicroPython and CircuitPython Unix — see [MicroPython — usdl2](../platforms/micropython.md#usdl2-native-sdl2). +SDL2 bindings for **`SDLDisplay`**: native **`usdl2`** when built in or installed from TestPyPI, else pure-Python **`usdl2-py`** / MIP ``usdl2`` from [PyDevices/usdl2](https://github.com/PyDevices/usdl2) (`lib/usdl2.py`). Build the native module for frozen MicroPython and CircuitPython Unix — see [MicroPython — usdl2](../platforms/micropython.md#usdl2-native-sdl2). ### PGDisplay diff --git a/docs/concepts/multimer.md b/docs/concepts/multimer.md index 0f333fe13..d6aa12a8b 100644 --- a/docs/concepts/multimer.md +++ b/docs/concepts/multimer.md @@ -178,10 +178,10 @@ Desktop SDL2 access is shared between display and timer code: | Consumer | Import chain | |----------|--------------| -| `displaysys.sdldisplay` | built-in `usdl2` → `add_ons/usdl2.py` | -| `multimer` SDL backend | `usdl2` (native or `add_ons/usdl2.py`) | +| `displaysys.sdldisplay` | built-in `usdl2` → pure-Python `usdl2-py` | +| `multimer` SDL backend | `usdl2` (native or `usdl2-py`) | -Both prefer the native **`usdl2`** module when it is frozen or built into the interpreter. See [Displays — SDLDisplay](displays.md#sdldisplay) and [MicroPython — usdl2](../platforms/micropython.md#usdl2-native-sdl2). +Both prefer the native **`usdl2`** module when it is frozen, built in, or installed from TestPyPI. Otherwise the pure-Python package from [PyDevices/usdl2](https://github.com/PyDevices/usdl2) (`lib/usdl2.py`) is used. See [Displays — SDLDisplay](displays.md#sdldisplay) and [MicroPython — usdl2](../platforms/micropython.md#usdl2-native-sdl2). ## Next diff --git a/docs/installation/mip-github.md b/docs/installation/mip-github.md index f0c7b208e..ca9fd4dc8 100644 --- a/docs/installation/mip-github.md +++ b/docs/installation/mip-github.md @@ -4,7 +4,9 @@ Install source `.py` files directly from the pydisplay GitHub repository using M ## Core install (recommended) -Install the four core libraries (and `path.py` from the displaysys package). +Install the three core libraries (`displaysys`, `eventsys`, `multimer`) plus +`graphics` (manifest still under `packages/graphics.json`, files hosted in +[PyDevices/graphics](https://github.com/PyDevices/graphics)). Board-specific `board_config.py` comes from a [`board_configs/`](https://github.com/PyDevices/micropython-hardware/tree/main/board_configs) package, not from displaysys: ```python @@ -21,6 +23,12 @@ for pkg in displaysys eventsys graphics multimer; do done ``` +Or install `graphics` from the MIP index published by the graphics repo: + +```python +mip.install("graphics", index="https://PyDevices.github.io/micropython-lib/mip/PyDevices") +``` + Add examples and add_ons separately: ```python @@ -36,7 +44,8 @@ Manifests live in the [`packages/`](https://github.com/PyDevices/pydisplay/tree/ |---------|----------| | displaysys | `packages/displaysys.json` | | eventsys | `packages/eventsys.json` | -| graphics | `packages/graphics.json` | +| graphics | `packages/graphics.json` (sources in PyDevices/graphics) | +| usdl2 | `packages/usdl2.json` (sources in PyDevices/usdl2) | | multimer | `packages/multimer.json` | | add_ons | `packages/add_ons.json` | | examples | `packages/examples.json` | diff --git a/docs/platforms/circuitpython.md b/docs/platforms/circuitpython.md index b72f86636..48ad6e088 100644 --- a/docs/platforms/circuitpython.md +++ b/docs/platforms/circuitpython.md @@ -104,9 +104,11 @@ directory exists. See [lv_circuitpython_mod README](https://github.com/PyDevices `multimer` supplies Adafruit-compatible `ticks_*` helpers for application code; frozen asyncio still uses `adafruit_ticks` internally unless the build is customized. -## framebuf shim +## framebuf -CircuitPython lacks MicroPython-compatible `framebuf`. Install `add_ons/framebuf.py` or copy it to your `lib/` folder. +CircuitPython lacks MicroPython-compatible `framebuf`. Use the `framebuf` module +from [PyDevices/graphics](https://github.com/PyDevices/graphics) +(`lib/graphics/framebuf.py`, MIP `graphics` / TestPyPI `graphics-py`). ## Installers diff --git a/docs/platforms/micropython.md b/docs/platforms/micropython.md index bece1b51c..0473bf70e 100644 --- a/docs/platforms/micropython.md +++ b/docs/platforms/micropython.md @@ -60,9 +60,9 @@ Use `board_configs/sdldisplay/` or the default `src/lib/board_config.py` for SDL For best SDL2 performance on MicroPython Unix, CircuitPython Unix, and `micropython.exe`, build with the optional native **`usdl2`** module from [PyDevices/usdl2](https://github.com/PyDevices/usdl2). It provides the SDL2 subset used by **`SDLDisplay`** and, when the host selects the SDL timer backend, **`multimer._sdl2`**. -Without **`usdl2`**, `SDLDisplay` falls back to pure-Python ffi/ctypes bindings; timer selection is unchanged (`multimer` still picks `_librt` or threading backends first on each platform). +Without the native module, `SDLDisplay` falls back to pure-Python ffi/ctypes bindings from the same repo (`usdl2-py` on TestPyPI / MIP `usdl2`); timer selection is unchanged (`multimer` still picks `_librt` or threading backends first on each platform). -On **CPython** desktop, the ctypes shim is published on TestPyPI as package **`usdl2`** (install with the [two-index pip pattern](../publishing-micropython-lib.md#two-index-pip-install-required); install it alongside `displaysys` when using `SDLDisplay` — it is not a MIP/`require()` dependency). +On **CPython** desktop, prefer the native TestPyPI wheel **`usdl2`**; the ctypes fallback is **`usdl2-py`** (same import name). Install either with the [two-index pip pattern](../publishing-micropython-lib.md#two-index-pip-install-required) alongside `displaysys` when using `SDLDisplay`. ## Frozen firmware diff --git a/docs/publishing-micropython-lib.md b/docs/publishing-micropython-lib.md index b64071460..8b1b9d0b5 100644 --- a/docs/publishing-micropython-lib.md +++ b/docs/publishing-micropython-lib.md @@ -57,7 +57,7 @@ Tags must match `v*.*.*` (e.g. `v0.0.5`, not `0.0.5`). Pushing the tag starts [**Publish micropython-lib**](https://github.com/PyDevices/pydisplay/actions/workflows/publish-micropython-lib.yml), which: 1. Syncs `src/lib/*` into [micropython-lib](https://github.com/PyDevices/micropython-lib) (`PyDevices` branch) at version `X.Y.Z` -2. Uploads CPython wheels to TestPyPI (`displaysys`, `eventsys`, `pydisplay-graphics`, `multimer`, …) +2. Uploads CPython wheels to TestPyPI (`displaysys`, `eventsys`, `multimer`, …). Pure-Python `graphics` publishes from [PyDevices/graphics](https://github.com/PyDevices/graphics) as `graphics-py`. 3. Rebuilds the [MIP index](https://PyDevices.github.io/micropython-lib/mip/PyDevices) on micropython-lib `gh-pages` Typical runtime: **~10–20 minutes**. @@ -192,7 +192,7 @@ Or `mpremote mip install --index "https://PyDevices.github.io/micropython-lib/mi ### TestPyPI -PyDevices CPython wheels are published to [TestPyPI](https://test.pypi.org) only (not production PyPI). Browse package names there (`displaysys`, `eventsys`, `multimer`, `pydisplay-graphics`, …). +PyDevices CPython wheels are published to [TestPyPI](https://test.pypi.org) only (not production PyPI). Browse package names there (`displaysys`, `eventsys`, `multimer`, `graphics-py`, …). **Naming:** MIP package names (e.g. `graphics`) may differ from the pip/TestPyPI project name when the MIP name is already taken on pypi.org. The mapping lives in `pypi_publish_name()` in [`publish_sync_packages.sh`](https://github.com/PyDevices/pydisplay/blob/main/scripts/publish_sync_packages.sh). @@ -268,7 +268,7 @@ Script options: `./scripts/publish_sync_packages.sh --help` |--------|------------| | Version already exists | Push a **new tag** with a higher semver — TestPyPI rejects duplicate versions | | Upload fails mid-run | Partial uploads may succeed; fix the error, bump the tag, push again | -| `graphics` sdist 400 | Name is taken on [pypi.org/project/graphics](https://pypi.org/project/graphics); PyPI project is `pydisplay-graphics` (MIP name stays `graphics`). See `pypi_publish_name()` in [`publish_sync_packages.sh`](https://github.com/PyDevices/pydisplay/blob/main/scripts/publish_sync_packages.sh). | +| `graphics` packaging | Pure-Python `graphics` / `graphics-py` is published from [PyDevices/graphics](https://github.com/PyDevices/graphics), not this repo. | | Slow | Normal — each lib package gets hatch build + twine upload | | Not for devices | Boards use the **MIP index**, not TestPyPI | diff --git a/packages/add_ons.json b/packages/add_ons.json index eda5fb13b..3d0d7f705 100644 --- a/packages/add_ons.json +++ b/packages/add_ons.json @@ -24,10 +24,6 @@ "fetch_ph_gui.py", "github:PyDevices/pydisplay/src/add_ons/fetch_ph_gui.py" ], - [ - "framebuf.py", - "github:PyDevices/pydisplay/src/add_ons/framebuf.py" - ], [ "hardware_setup.py", "github:PyDevices/pydisplay/src/add_ons/hardware_setup.py" @@ -80,10 +76,6 @@ "uctypes.py", "github:PyDevices/pydisplay/src/add_ons/uctypes.py" ], - [ - "usdl2.py", - "github:PyDevices/pydisplay/src/add_ons/usdl2.py" - ], [ "viper_tools.py", "github:PyDevices/pydisplay/src/add_ons/viper_tools.py" diff --git a/packages/graphics.json b/packages/graphics.json index e02d47914..69d6280e6 100644 --- a/packages/graphics.json +++ b/packages/graphics.json @@ -2,57 +2,61 @@ "urls": [ [ "graphics/__init__.py", - "github:PyDevices/pydisplay/src/lib/graphics/__init__.py" + "github:PyDevices/graphics/lib/graphics/__init__.py" ], [ "graphics/_area.py", - "github:PyDevices/pydisplay/src/lib/graphics/_area.py" + "github:PyDevices/graphics/lib/graphics/_area.py" ], [ "graphics/_blit_hooks.py", - "github:PyDevices/pydisplay/src/lib/graphics/_blit_hooks.py" + "github:PyDevices/graphics/lib/graphics/_blit_hooks.py" ], [ "graphics/_bmp565.py", - "github:PyDevices/pydisplay/src/lib/graphics/_bmp565.py" + "github:PyDevices/graphics/lib/graphics/_bmp565.py" ], [ "graphics/_clip.py", - "github:PyDevices/pydisplay/src/lib/graphics/_clip.py" + "github:PyDevices/graphics/lib/graphics/_clip.py" ], [ "graphics/_draw.py", - "github:PyDevices/pydisplay/src/lib/graphics/_draw.py" + "github:PyDevices/graphics/lib/graphics/_draw.py" ], [ "graphics/_files.py", - "github:PyDevices/pydisplay/src/lib/graphics/_files.py" + "github:PyDevices/graphics/lib/graphics/_files.py" ], [ "graphics/_font.py", - "github:PyDevices/pydisplay/src/lib/graphics/_font.py" + "github:PyDevices/graphics/lib/graphics/_font.py" ], [ "graphics/_font_8x14.py", - "github:PyDevices/pydisplay/src/lib/graphics/_font_8x14.py" + "github:PyDevices/graphics/lib/graphics/_font_8x14.py" ], [ "graphics/_font_8x16.py", - "github:PyDevices/pydisplay/src/lib/graphics/_font_8x16.py" + "github:PyDevices/graphics/lib/graphics/_font_8x16.py" ], [ "graphics/_font_8x8.py", - "github:PyDevices/pydisplay/src/lib/graphics/_font_8x8.py" + "github:PyDevices/graphics/lib/graphics/_font_8x8.py" ], [ "graphics/_framebuf_plus.py", - "github:PyDevices/pydisplay/src/lib/graphics/_framebuf_plus.py" + "github:PyDevices/graphics/lib/graphics/_framebuf_plus.py" ], [ "graphics/_shapes.py", - "github:PyDevices/pydisplay/src/lib/graphics/_shapes.py" + "github:PyDevices/graphics/lib/graphics/_shapes.py" + ], + [ + "graphics/framebuf.py", + "github:PyDevices/graphics/lib/graphics/framebuf.py" ] ], "deps": [], "version": "0.0.1" -} \ No newline at end of file +} diff --git a/packages/usdl2.json b/packages/usdl2.json new file mode 100644 index 000000000..55e80b820 --- /dev/null +++ b/packages/usdl2.json @@ -0,0 +1,10 @@ +{ + "urls": [ + [ + "usdl2.py", + "github:PyDevices/usdl2/lib/usdl2.py" + ] + ], + "deps": [], + "version": "0.0.1" +} diff --git a/scripts/README.md b/scripts/README.md index d4b7ca07c..58d472bad 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -104,8 +104,6 @@ python scripts/gallery_generator.py python scripts/gallery_generator.py --check # CI freshness # After editing src/add_ons/framebuf.py: -python scripts/install_sync_framebuf.py # regenerate src/lib/graphics/framebuf.py -python scripts/install_sync_framebuf.py --check # CI freshness (also run via tests/test_framebuf_sync.py) # After public-API changes in displaysys / eventsys / graphics / multimer: ./scripts/gen_package_pyi.sh # regenerate tools/typings// stubs @@ -115,7 +113,6 @@ python scripts/install_sync_framebuf.py --check # CI freshness (also run via | Prefix | Scripts | When to run | |--------|---------|-------------| -| `install_` | `install_gen_manifests.py`, `install_refresh_manifests.sh`, `install_sync_framebuf.py` | `src/` tree changes → updates `packages/*.json`, `web/pyscript/micropython.toml`; `src/add_ons/framebuf.py` changes → regenerates gitignored `src/lib/graphics/framebuf.py` | | `gen_` | `gen_package_pyi.sh` | Core package API changes → regenerates `tools/typings/{displaysys,eventsys,graphics,multimer}/` | | `pyscript_` | `gallery_generator.py` | Gallery cards in `web/pyscript/index.html` | | `mkdocs_` | `mkdocs_gen_ref_pages.py`, `mkdocs_gen_notebook_pages.py` | Automatically on `mkdocs build` | diff --git a/scripts/gen_package_pyi.sh b/scripts/gen_package_pyi.sh index 593150feb..b8e8a470f 100755 --- a/scripts/gen_package_pyi.sh +++ b/scripts/gen_package_pyi.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash -# Regenerate IDE type stubs for the four core pydisplay packages into tools/typings/. +# Regenerate IDE type stubs for the three core pydisplay packages into tools/typings/. # # Usage: # ./scripts/gen_package_pyi.sh # ./scripts/gen_package_pyi.sh --help # # Requires the repo-root .venv (mypy / stubgen). Output is committed under -# tools/typings/{displaysys,eventsys,graphics,multimer}/ for stubPath. +# tools/typings/{displaysys,eventsys,multimer}/ for stubPath. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -PACKAGES=(displaysys eventsys graphics multimer) +PACKAGES=(displaysys eventsys multimer) OUT=tools/typings STUBGEN="${ROOT}/.venv/bin/stubgen" PYTHON="${ROOT}/.venv/bin/python" @@ -22,10 +22,9 @@ usage() { cat <<'EOF' Usage: ./scripts/gen_package_pyi.sh -Regenerate mypy stubgen .pyi trees for displaysys, eventsys, graphics, and +Regenerate mypy stubgen .pyi trees for displaysys, eventsys, and multimer into tools/typings/ (Pylance / pyright stubPath). -Requires .venv with mypy (stubgen). Drops graphics/framebuf.pyi if produced (source is gitignored; public API is graphics.FrameBuffer). EOF } @@ -57,10 +56,9 @@ export PYTHONPATH="${ROOT}/src/lib:${ROOT}/src/add_ons${PYTHONPATH:+:$PYTHONPATH echo "Running stubgen → ${OUT}/ …" "$STUBGEN" --ignore-errors -o "$OUT" \ - -p displaysys -p eventsys -p graphics -p multimer + -p displaysys -p eventsys -p multimer # Gitignored generated module; public FrameBuffer lives in _framebuf_plus. -rm -f "${OUT}/graphics/framebuf.pyi" # stubgen misses lazy ``multimer.asyncio`` (``__getattr__``); declare it explicitly. MULTIMER_INIT="${OUT}/multimer/__init__.pyi" diff --git a/scripts/install_gen_manifests.py b/scripts/install_gen_manifests.py index dc356500c..9e44a0301 100755 --- a/scripts/install_gen_manifests.py +++ b/scripts/install_gen_manifests.py @@ -27,10 +27,29 @@ ["examples", [], []], ["lib/displaysys", [], ["path.py"]], ["lib/eventsys", [], []], - ["lib/graphics", [], []], ["lib/multimer", [], []], ] +# Pure-Python graphics lives in sibling PyDevices/graphics (lib/graphics/). +# packages/graphics.json is maintained there / by hand; PyScript mounts below. +GRAPHICS_SIBLING_MOUNT = "/lib/graphics/" +GRAPHICS_SIBLING_FILES = [ + "__init__.py", + "_area.py", + "_blit_hooks.py", + "_bmp565.py", + "_clip.py", + "_draw.py", + "_files.py", + "_font.py", + "_font_8x14.py", + "_font_8x16.py", + "_font_8x8.py", + "_framebuf_plus.py", + "_shapes.py", + "framebuf.py", +] + # Packages omitted from web/pyscript/micropython.toml (PyScript mounts add_ons for browser examples). toml_exclude = ["examples"] @@ -211,6 +230,13 @@ def pyscript_toml_file_entry(repo_relative_path: str, mount: str) -> str: json.dump({"urls": urls, "version": package_ver}, f, indent=2) example_package_names.append(entry) +# Sibling graphics package (not under this repo's src/). +master_toml.append("") +for name in GRAPHICS_SIBLING_FILES: + master_toml.append( + f'"../graphics/lib/graphics/{name}" = "{GRAPHICS_SIBLING_MOUNT}"' + ) + # Gallery loaders use `import ps_loader` (top-level); also mount at VFS root. master_toml.append(pyscript_toml_file_entry("src/add_ons/ps_loader.py", "/")) diff --git a/scripts/install_sync_framebuf.py b/scripts/install_sync_framebuf.py deleted file mode 100755 index 662f2b75e..000000000 --- a/scripts/install_sync_framebuf.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Copy canonical ``src/add_ons/framebuf.py`` into ``graphics/`` for packaging. - -``src/add_ons/framebuf.py`` is the only editable source. ``src/lib/graphics/framebuf.py`` -is generated (gitignored) so the pydisplay-graphics wheel and standalone ``graphics`` -copytree tests stay self-contained without maintaining two copies by hand. - -Usage: - .venv/bin/python scripts/install_sync_framebuf.py # write copy - .venv/bin/python scripts/install_sync_framebuf.py --check # fail if missing or stale -""" - -from __future__ import annotations - -import argparse -from pathlib import Path -import shutil -import sys - -_REPO_ROOT = Path(__file__).resolve().parents[1] -_CANONICAL = _REPO_ROOT / "src" / "add_ons" / "framebuf.py" -_GENERATED = _REPO_ROOT / "src" / "lib" / "graphics" / "framebuf.py" - - -def sync_framebuf(*, check: bool = False) -> None: - if not _CANONICAL.is_file(): - raise SystemExit(f"missing canonical framebuf module: {_CANONICAL}") - - canonical = _CANONICAL.read_bytes() - - if check: - if not _GENERATED.is_file(): - raise SystemExit( - f"missing {_GENERATED.relative_to(_REPO_ROOT)} — " - "run: .venv/bin/python scripts/install_sync_framebuf.py" - ) - if _GENERATED.read_bytes() != canonical: - raise SystemExit( - f"{_GENERATED.relative_to(_REPO_ROOT)} is stale — " - "edit src/add_ons/framebuf.py, then run scripts/install_sync_framebuf.py" - ) - return - - _GENERATED.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(_CANONICAL, _GENERATED) - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--check", - action="store_true", - help="exit non-zero if the generated copy is missing or differs from canonical", - ) - args = parser.parse_args(argv) - sync_framebuf(check=args.check) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/publish_sync_packages.sh b/scripts/publish_sync_packages.sh index 9cfaf8ed2..f19b2e089 100755 --- a/scripts/publish_sync_packages.sh +++ b/scripts/publish_sync_packages.sh @@ -117,9 +117,6 @@ if [[ -z "$VERSION" ]]; then fi echo "Release version: $VERSION" -# graphics.framebuf is generated from add_ons/framebuf.py before packaging. -"$SOURCE_REPO/scripts/install_sync_framebuf.py" - AUTHOR="Brad Barnett " LICENSE="MIT" @@ -152,14 +149,11 @@ should_skip_name() { esac } -# MIP package names (eventsys, graphics, …) may differ from PyPI project names. -# TestPyPI rejects sdists for names already registered on pypi.org (e.g. graphics). +# MIP package names may differ from PyPI project names when needed. +# graphics publishes from PyDevices/graphics (graphics-py), not this repo. # Convention: .cursor/testpypi-naming-convention.md pypi_publish_name() { - case "$1" in - graphics) echo "pydisplay-graphics" ;; - *) echo "$1" ;; - esac + echo "$1" } # Short PyPI summary (one line) — not the monorepo tagline. @@ -174,9 +168,6 @@ package_summary() { multimer) echo "Cross-platform machine.Timer-style and asyncio timers for MicroPython and CPython" ;; - graphics) - echo "Pure-Python graphics for pydisplay (FrameBuffer, Draw, fonts); import as graphics" - ;; *) echo "PyDisplay $1" ;; diff --git a/scripts/setup_sibling_repos.sh b/scripts/setup_sibling_repos.sh index 6078cbabb..c160b571d 100755 --- a/scripts/setup_sibling_repos.sh +++ b/scripts/setup_sibling_repos.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Clone palettes / pdwidgets siblings for local dev and example tests. +# Clone palettes / pdwidgets / graphics / usdl2 siblings for local dev and example tests. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -22,14 +22,22 @@ clone_or_update() { PALETTES="$(clone_or_update palettes)" PDWIDGETS="$(clone_or_update pdwidgets)" +GRAPHICS="$(clone_or_update graphics)" +USDL2="$(clone_or_update usdl2)" SITE="$("$ROOT/.venv/bin/python" -c 'import site; print(site.getsitepackages()[0])')" -echo "$PALETTES/src" >"$SITE/palettes.pth" -echo "$PDWIDGETS/src" >"$SITE/pdwidgets.pth" +echo "$PALETTES/lib" >"$SITE/palettes.pth" +echo "$PDWIDGETS/lib" >"$SITE/pdwidgets.pth" +echo "$GRAPHICS/lib" >"$SITE/graphics.pth" +echo "$USDL2/lib" >"$SITE/usdl2.pth" -export PYDISPLAY_PALETTES_SRC="$PALETTES/src" -export PYDISPLAY_PDWIDGETS_SRC="$PDWIDGETS/src" +export PYDISPLAY_PALETTES_LIB="$PALETTES/lib" +export PYDISPLAY_PDWIDGETS_LIB="$PDWIDGETS/lib" +export PYDISPLAY_GRAPHICS_LIB="$GRAPHICS/lib" +export PYDISPLAY_USDL2_LIB="$USDL2/lib" -echo "palettes: $PALETTES/src" -echo "pdwidgets: $PDWIDGETS/src" +echo "palettes: $PALETTES/lib" +echo "pdwidgets: $PDWIDGETS/lib" +echo "graphics: $GRAPHICS/lib" +echo "usdl2: $USDL2/lib" echo "CPython .pth files written under $SITE" diff --git a/src/add_ons/framebuf.py b/src/add_ons/framebuf.py deleted file mode 100644 index 3821187b2..000000000 --- a/src/add_ons/framebuf.py +++ /dev/null @@ -1,786 +0,0 @@ -# SPDX-FileCopyrightText: 2016 Damien P. George -# Copyright 2024-2026 Brad Barnett -# SPDX-License-Identifier: MIT - -""" -framebuf.py - a pure-Python, drop-in replacement for MicroPython's built-in -``framebuf`` module (https://docs.micropython.org/en/latest/library/framebuf.html) -for use on CPython and CircuitPython. - -This module intentionally mirrors MicroPython's C implementation -(``extmod/modframebuf.c``, by Damien P. George) as closely as possible: - -- Same format constants, same method signatures, same return values (``None`` - for every drawing method; only ``pixel()`` without a color argument returns - a value). -- No pydisplay extensions (no ``RGB888``, no ``Area`` return values, no - ``arc``/``circle``/``blit_rect``/etc.) and no imports from ``graphics``. - -For the pydisplay-flavored subclass that adds ``Area`` bounding boxes, -``RGB888``, extra shapes, and file I/O, see ``graphics.FrameBuffer`` -(``graphics/_framebuf_plus.py``), which subclasses this module. -""" - -try: - from ulab import numpy as np -except ImportError: - try: - import numpy as np - except ImportError: - np = None - - -# Framebuf format constants (values match MicroPython's framebuf module): -MONO_VLSB = 0 -MVLSB = 0 -RGB565 = 1 -GS4_HMSB = 2 -MONO_HLSB = 3 -MONO_HMSB = 4 -GS2_HMSB = 5 -GS8 = 6 - - -class MVLSBFormat: - """Single bit displays, vertically mapped (e.g. SSD1306 OLED).""" - - depth = 1 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (y >> 3) * fb._stride + x - offset = y & 0x07 - fb._buffer[index] = (fb._buffer[index] & ~(0x01 << offset) & 0xFF) | ( - (color != 0) << offset - ) - - @staticmethod - def get_pixel(fb, x, y): - index = (y >> 3) * fb._stride + x - offset = y & 0x07 - return (fb._buffer[index] >> offset) & 0x01 - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - while height > 0: - index = (y >> 3) * fb._stride + x - offset = y & 0x07 - for w_w in range(width): - fb._buffer[index + w_w] = (fb._buffer[index + w_w] & ~(0x01 << offset) & 0xFF) | ( - (color != 0) << offset - ) - y += 1 - height -= 1 - - -class MHLSBFormat: - """Single bit displays, horizontally mapped, bit 0 = rightmost pixel (e.g. PBM).""" - - depth = 1 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (x + y * fb._stride) >> 3 - offset = 7 - (x & 0x07) - fb._buffer[index] = (fb._buffer[index] & ~(0x01 << offset) & 0xFF) | ( - (color != 0) << offset - ) - - @staticmethod - def get_pixel(fb, x, y): - index = (x + y * fb._stride) >> 3 - offset = 7 - (x & 0x07) - return (fb._buffer[index] >> offset) & 0x01 - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - advance = fb._stride >> 3 - col = 1 if color else 0 - while width: - index = (x >> 3) + y * advance - offset = 7 - (x & 0x07) - bit = 0x01 << offset - for _ in range(height): - fb._buffer[index] = (fb._buffer[index] & ~bit & 0xFF) | (col << offset) - index += advance - x += 1 - width -= 1 - - -class MHMSBFormat: - """Single bit displays, horizontally mapped, bit 0 = leftmost pixel.""" - - depth = 1 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (x + y * fb._stride) >> 3 - offset = x & 0x07 - fb._buffer[index] = (fb._buffer[index] & ~(0x01 << offset) & 0xFF) | ( - (color != 0) << offset - ) - - @staticmethod - def get_pixel(fb, x, y): - index = (x + y * fb._stride) >> 3 - offset = x & 0x07 - return (fb._buffer[index] >> offset) & 0x01 - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - advance = fb._stride >> 3 - col = 1 if color else 0 - while width: - index = (x >> 3) + y * advance - offset = x & 0x07 - bit = 0x01 << offset - for _ in range(height): - fb._buffer[index] = (fb._buffer[index] & ~bit & 0xFF) | (col << offset) - index += advance - x += 1 - width -= 1 - - -class GS2HMSBFormat: - """2-bit grayscale, horizontally mapped MSB-first (e.g. HT16K33 8x8 Matrix).""" - - depth = 2 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (x + y * fb._stride) >> 2 - pixel = fb._buffer[index] - shift = (x & 0b11) << 1 - mask = 0b11 << shift - color = (color & 0b11) << shift - fb._buffer[index] = color | (pixel & (~mask & 0xFF)) - - @staticmethod - def get_pixel(fb, x, y): - index = (x + y * fb._stride) >> 2 - pixel = fb._buffer[index] - shift = (x & 0b11) << 1 - return (pixel >> shift) & 0b11 - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - for _x in range(x, x + width): - for _y in range(y, y + height): - GS2HMSBFormat.set_pixel(fb, _x, _y, color) - - -class GS4HMSBFormat: - """4-bit grayscale, horizontally mapped, two pixels per byte.""" - - depth = 4 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (x + y * fb._stride) >> 1 - if x & 0x01: - fb._buffer[index] = (color & 0x0F) | (fb._buffer[index] & 0xF0) - else: - fb._buffer[index] = ((color & 0x0F) << 4) | (fb._buffer[index] & 0x0F) - - @staticmethod - def get_pixel(fb, x, y): - index = (x + y * fb._stride) >> 1 - if x & 0x01: - return fb._buffer[index] & 0x0F - return fb._buffer[index] >> 4 - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - for _y in range(y, y + height): - for _x in range(x, x + width): - GS4HMSBFormat.set_pixel(fb, _x, _y, color) - - -class GS8Format: - """8-bit grayscale/palette, one byte per pixel.""" - - depth = 8 - - @staticmethod - def set_pixel(fb, x, y, color): - fb._buffer[y * fb._stride + x] = color & 0xFF - - @staticmethod - def get_pixel(fb, x, y): - return fb._buffer[y * fb._stride + x] - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - row = bytes((color & 0xFF,)) * width - for _y in range(y, y + height): - index = _y * fb._stride + x - fb._buffer[index : index + width] = row - - -class RGB565Format: - """16-bit color, two bytes per pixel, little-endian.""" - - depth = 16 - - @staticmethod - def set_pixel(fb, x, y, color): - index = (x + y * fb._stride) * 2 - fb._buffer[index : index + 2] = (color & 0xFFFF).to_bytes(2, "little") - - @staticmethod - def get_pixel(fb, x, y): - index = (x + y * fb._stride) * 2 - return int.from_bytes(fb._buffer[index : index + 2], "little") - - @staticmethod - def fill_rect(fb, x, y, width, height, color): - stride = fb._stride - rgb565_color = (color & 0xFFFF).to_bytes(2, "little") - if np: - rgb565_color_int = int.from_bytes(rgb565_color, "little") - arr = np.frombuffer(fb._buffer, dtype=np.uint16) - for _y in range(y, y + height): - arr[_y * stride + x : _y * stride + x + width] = rgb565_color_int - else: - for _y in range(y, y + height): - offset = _y * stride - for _x in range(x, x + width): - index = (offset + _x) * 2 - fb._buffer[index : index + 2] = rgb565_color - - -def _setpixel_checked(fb, x, y, color, mask): - if mask and 0 <= x < fb._width and 0 <= y < fb._height: - fb._format.set_pixel(fb, x, y, color) - - -def _fill_rect(fb, x, y, w, h, color): - """Clip ``x, y, w, h`` to the framebuffer bounds, then delegate to the format.""" - if h < 1 or w < 1 or x + w <= 0 or y + h <= 0 or y >= fb._height or x >= fb._width: - return - xend = min(fb._width, x + w) - yend = min(fb._height, y + h) - x = max(x, 0) - y = max(y, 0) - fb._format.fill_rect(fb, x, y, xend - x, yend - y, color) - - -def _line(fb, x1, y1, x2, y2, c): - dx = x2 - x1 - if dx > 0: - sx = 1 - else: - dx = -dx - sx = -1 - - dy = y2 - y1 - if dy > 0: - sy = 1 - else: - dy = -dy - sy = -1 - - steep = dy > dx - if steep: - x1, y1 = y1, x1 - dx, dy = dy, dx - sx, sy = sy, sx - - e = 2 * dy - dx - for _ in range(dx): - if steep: - if 0 <= y1 < fb._width and 0 <= x1 < fb._height: - fb._format.set_pixel(fb, y1, x1, c) - else: - if 0 <= x1 < fb._width and 0 <= y1 < fb._height: - fb._format.set_pixel(fb, x1, y1, c) - while e >= 0: - y1 += sy - e -= 2 * dx - x1 += sx - e += 2 * dy - - _setpixel_checked(fb, x2, y2, c, 1) - - -_ELLIPSE_MASK_FILL = 0x10 -_ELLIPSE_MASK_ALL = 0x0F - - -def _draw_ellipse_points(fb, cx, cy, x, y, c, mask): - if mask & _ELLIPSE_MASK_FILL: - if mask & 0x1: - _fill_rect(fb, cx, cy - y, x + 1, 1, c) - if mask & 0x2: - _fill_rect(fb, cx - x, cy - y, x + 1, 1, c) - if mask & 0x4: - _fill_rect(fb, cx - x, cy + y, x + 1, 1, c) - if mask & 0x8: - _fill_rect(fb, cx, cy + y, x + 1, 1, c) - else: - _setpixel_checked(fb, cx + x, cy - y, c, mask & 0x1) - _setpixel_checked(fb, cx - x, cy - y, c, mask & 0x2) - _setpixel_checked(fb, cx - x, cy + y, c, mask & 0x4) - _setpixel_checked(fb, cx + x, cy + y, c, mask & 0x8) - - -def _ellipse(fb, cx, cy, xr, yr, c, f=False, m=_ELLIPSE_MASK_ALL): - mask = (_ELLIPSE_MASK_FILL if f else 0) | (m & _ELLIPSE_MASK_ALL) - if xr == 0 and yr == 0: - _setpixel_checked(fb, cx, cy, c, mask & _ELLIPSE_MASK_ALL) - return - - two_asquare = 2 * xr * xr - two_bsquare = 2 * yr * yr - x = xr - y = 0 - xchange = yr * yr * (1 - 2 * xr) - ychange = xr * xr - ellipse_error = 0 - stoppingx = two_bsquare * xr - stoppingy = 0 - while stoppingx >= stoppingy: - _draw_ellipse_points(fb, cx, cy, x, y, c, mask) - y += 1 - stoppingy += two_asquare - ellipse_error += ychange - ychange += two_asquare - if (2 * ellipse_error + xchange) > 0: - x -= 1 - stoppingx -= two_bsquare - ellipse_error += xchange - xchange += two_bsquare - - x = 0 - y = yr - xchange = yr * yr - ychange = xr * xr * (1 - 2 * yr) - ellipse_error = 0 - stoppingx = 0 - stoppingy = two_asquare * yr - while stoppingx <= stoppingy: - _draw_ellipse_points(fb, cx, cy, x, y, c, mask) - x += 1 - stoppingx += two_bsquare - ellipse_error += xchange - xchange += two_bsquare - if (2 * ellipse_error + ychange) > 0: - y -= 1 - stoppingy -= two_asquare - ellipse_error += ychange - ychange += two_asquare - - -def _trunc_div(a, b): - """Integer division truncated toward zero, matching C's ``/`` for ints.""" - q = a // b - if (a % b != 0) and ((a < 0) != (b < 0)): - q += 1 - return q - - -def _poly_points(coords): - """Normalize ``coords`` (a flat array/list of ints, or a list of (x, y) pairs).""" - coords = list(coords) - if coords and isinstance(coords[0], (list, tuple)): - return [(int(px), int(py)) for px, py in coords] - if len(coords) % 2: - coords = coords[:-1] - return [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] - - -def _poly_edges(pts): - """Yield (px1, py1, px2, py2) edges in the exact order MicroPython visits them: - point[0] -> point[n-1] -> point[n-2] -> ... -> point[1] -> point[0]. - - Line drawing is not always symmetric under endpoint reversal for steep - segments (integer rounding), so this order must match ``modframebuf.c`` - exactly for pixel-identical output. - """ - px1, py1 = pts[0] - for k in range(len(pts) - 1, -1, -1): - px2, py2 = pts[k] - yield px1, py1, px2, py2 - px1, py1 = px2, py2 - - -def _poly(fb, x, y, coords, c, f=False): - pts = _poly_points(coords) - if not pts: - return - edges = list(_poly_edges(pts)) - - if f: - y_min = min(p[1] for p in pts) - y_max = max(p[1] for p in pts) - for row in range(y_min, y_max + 1): - nodes = [] - for px1, py1, px2, py2 in edges: - if py1 != py2 and ((py1 > row and py2 <= row) or (py1 <= row and py2 > row)): - node = _trunc_div( - 32 * px1 + _trunc_div(32 * (px2 - px1) * (row - py1), py2 - py1) + 16, - 32, - ) - nodes.append(node) - elif row == max(py1, py2): - if py1 < py2: - _setpixel_checked(fb, x + px2, y + py2, c, 1) - elif py2 < py1: - _setpixel_checked(fb, x + px1, y + py1, c, 1) - else: - _line(fb, x + px1, y + py1, x + px2, y + py2, c) - if not nodes: - continue - nodes.sort() - for i in range(0, len(nodes) - 1, 2): - _fill_rect(fb, x + nodes[i], y + row, nodes[i + 1] - nodes[i] + 1, 1, c) - else: - for px1, py1, px2, py2 in edges: - _line(fb, x + px1, y + py1, x + px2, y + py2, c) - - -def _scroll(fb, xstep, ystep): - width = fb._width - height = fb._height - - if xstep < 0: - if -xstep >= width: - return - sx = 0 - xend = width + xstep - dx = 1 - else: - if xstep >= width: - return - sx = width - 1 - xend = xstep - 1 - dx = -1 - - if ystep < 0: - if -ystep >= height: - return - y = 0 - yend = height + ystep - dy = 1 - else: - if ystep >= height: - return - y = height - 1 - yend = ystep - 1 - dy = -1 - - while y != yend: - x = sx - while x != xend: - fb._format.set_pixel(fb, x, y, fb._format.get_pixel(fb, x - xstep, y - ystep)) - x += dx - y += dy - - -def _as_readonly_framebuffer(arg): - """Accept a ``FrameBuffer`` or a ``(buffer, width, height, format[, stride])`` tuple/list.""" - if hasattr(arg, "width") and hasattr(arg, "height") and hasattr(arg, "pixel"): - return arg - items = list(arg) - if len(items) < 4 or len(items) > 5: - raise ValueError("invalid framebuffer arguments") - return FrameBuffer(*items) - - -def _blit(fb, source, x, y, key=-1, palette=None): - source = _as_readonly_framebuffer(source) - if palette is not None: - palette = _as_readonly_framebuffer(palette) - - if x >= fb._width or y >= fb._height or -x >= source.width or -y >= source.height: - return - - x0 = max(0, x) - y0 = max(0, y) - x1 = max(0, -x) - y1 = max(0, -y) - x0end = min(fb._width, x + source.width) - y0end = min(fb._height, y + source.height) - - for cy0 in range(y0, y0end): - cx1 = x1 - for cx0 in range(x0, x0end): - col = source.pixel(cx1, y1) - if palette is not None: - col = palette.pixel(col, 0) - if col != key: - fb._format.set_pixel(fb, cx0, cy0, col) - cx1 += 1 - y1 += 1 - - -# font_petme128_8x8: 96 glyphs (ASCII 32-127), 8 bytes each. Each byte is one -# vertical column of 8 pixels with bit 0 at the top. Ported byte-for-byte from -# MicroPython's extmod/font_petme128_8x8.h (Damien P. George). -_FONT_PETME128_8X8 = ( - b"\x00\x00\x00\x00\x00\x00\x00\x00" # 32= - b"\x00\x00\x00\x4f\x4f\x00\x00\x00" # 33=! - b"\x00\x07\x07\x00\x00\x07\x07\x00" # 34=" - b"\x14\x7f\x7f\x14\x14\x7f\x7f\x14" # 35=# - b"\x00\x24\x2e\x6b\x6b\x3a\x12\x00" # 36=$ - b"\x00\x63\x33\x18\x0c\x66\x63\x00" # 37=% - b"\x00\x32\x7f\x4d\x4d\x77\x72\x50" # 38=& - b"\x00\x00\x00\x04\x06\x03\x01\x00" # 39=' - b"\x00\x00\x1c\x3e\x63\x41\x00\x00" # 40=( - b"\x00\x00\x41\x63\x3e\x1c\x00\x00" # 41=) - b"\x08\x2a\x3e\x1c\x1c\x3e\x2a\x08" # 42=* - b"\x00\x08\x08\x3e\x3e\x08\x08\x00" # 43=+ - b"\x00\x00\x80\xe0\x60\x00\x00\x00" # 44=, - b"\x00\x08\x08\x08\x08\x08\x08\x00" # 45=- - b"\x00\x00\x00\x60\x60\x00\x00\x00" # 46=. - b"\x00\x40\x60\x30\x18\x0c\x06\x02" # 47=/ - b"\x00\x3e\x7f\x49\x45\x7f\x3e\x00" # 48=0 - b"\x00\x40\x44\x7f\x7f\x40\x40\x00" # 49=1 - b"\x00\x62\x73\x51\x49\x4f\x46\x00" # 50=2 - b"\x00\x22\x63\x49\x49\x7f\x36\x00" # 51=3 - b"\x00\x18\x18\x14\x16\x7f\x7f\x10" # 52=4 - b"\x00\x27\x67\x45\x45\x7d\x39\x00" # 53=5 - b"\x00\x3e\x7f\x49\x49\x7b\x32\x00" # 54=6 - b"\x00\x03\x03\x79\x7d\x07\x03\x00" # 55=7 - b"\x00\x36\x7f\x49\x49\x7f\x36\x00" # 56=8 - b"\x00\x26\x6f\x49\x49\x7f\x3e\x00" # 57=9 - b"\x00\x00\x00\x24\x24\x00\x00\x00" # 58=: - b"\x00\x00\x80\xe4\x64\x00\x00\x00" # 59=; - b"\x00\x08\x1c\x36\x63\x41\x41\x00" # 60=< - b"\x00\x14\x14\x14\x14\x14\x14\x00" # 61== - b"\x00\x41\x41\x63\x36\x1c\x08\x00" # 62=> - b"\x00\x02\x03\x51\x59\x0f\x06\x00" # 63=? - b"\x00\x3e\x7f\x41\x4d\x4f\x2e\x00" # 64=@ - b"\x00\x7c\x7e\x0b\x0b\x7e\x7c\x00" # 65=A - b"\x00\x7f\x7f\x49\x49\x7f\x36\x00" # 66=B - b"\x00\x3e\x7f\x41\x41\x63\x22\x00" # 67=C - b"\x00\x7f\x7f\x41\x63\x3e\x1c\x00" # 68=D - b"\x00\x7f\x7f\x49\x49\x41\x41\x00" # 69=E - b"\x00\x7f\x7f\x09\x09\x01\x01\x00" # 70=F - b"\x00\x3e\x7f\x41\x49\x7b\x3a\x00" # 71=G - b"\x00\x7f\x7f\x08\x08\x7f\x7f\x00" # 72=H - b"\x00\x00\x41\x7f\x7f\x41\x00\x00" # 73=I - b"\x00\x20\x60\x41\x7f\x3f\x01\x00" # 74=J - b"\x00\x7f\x7f\x1c\x36\x63\x41\x00" # 75=K - b"\x00\x7f\x7f\x40\x40\x40\x40\x00" # 76=L - b"\x00\x7f\x7f\x06\x0c\x06\x7f\x7f" # 77=M - b"\x00\x7f\x7f\x0e\x1c\x7f\x7f\x00" # 78=N - b"\x00\x3e\x7f\x41\x41\x7f\x3e\x00" # 79=O - b"\x00\x7f\x7f\x09\x09\x0f\x06\x00" # 80=P - b"\x00\x1e\x3f\x21\x61\x7f\x5e\x00" # 81=Q - b"\x00\x7f\x7f\x19\x39\x6f\x46\x00" # 82=R - b"\x00\x26\x6f\x49\x49\x7b\x32\x00" # 83=S - b"\x00\x01\x01\x7f\x7f\x01\x01\x00" # 84=T - b"\x00\x3f\x7f\x40\x40\x7f\x3f\x00" # 85=U - b"\x00\x1f\x3f\x60\x60\x3f\x1f\x00" # 86=V - b"\x00\x7f\x7f\x30\x18\x30\x7f\x7f" # 87=W - b"\x00\x63\x77\x1c\x1c\x77\x63\x00" # 88=X - b"\x00\x07\x0f\x78\x78\x0f\x07\x00" # 89=Y - b"\x00\x61\x71\x59\x4d\x47\x43\x00" # 90=Z - b"\x00\x00\x7f\x7f\x41\x41\x00\x00" # 91=[ - b"\x00\x02\x06\x0c\x18\x30\x60\x40" # 92= - b"\x00\x00\x41\x41\x7f\x7f\x00\x00" # 93=] - b"\x00\x08\x0c\x06\x06\x0c\x08\x00" # 94=^ - b"\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0" # 95=_ - b"\x00\x00\x01\x03\x06\x04\x00\x00" # 96=` - b"\x00\x20\x74\x54\x54\x7c\x78\x00" # 97=a - b"\x00\x7f\x7f\x44\x44\x7c\x38\x00" # 98=b - b"\x00\x38\x7c\x44\x44\x6c\x28\x00" # 99=c - b"\x00\x38\x7c\x44\x44\x7f\x7f\x00" # 100=d - b"\x00\x38\x7c\x54\x54\x5c\x58\x00" # 101=e - b"\x00\x08\x7e\x7f\x09\x03\x02\x00" # 102=f - b"\x00\x98\xbc\xa4\xa4\xfc\x7c\x00" # 103=g - b"\x00\x7f\x7f\x04\x04\x7c\x78\x00" # 104=h - b"\x00\x00\x00\x7d\x7d\x00\x00\x00" # 105=i - b"\x00\x40\xc0\x80\x80\xfd\x7d\x00" # 106=j - b"\x00\x7f\x7f\x30\x38\x6c\x44\x00" # 107=k - b"\x00\x00\x41\x7f\x7f\x40\x00\x00" # 108=l - b"\x00\x7c\x7c\x18\x30\x18\x7c\x7c" # 109=m - b"\x00\x7c\x7c\x04\x04\x7c\x78\x00" # 110=n - b"\x00\x38\x7c\x44\x44\x7c\x38\x00" # 111=o - b"\x00\xfc\xfc\x24\x24\x3c\x18\x00" # 112=p - b"\x00\x18\x3c\x24\x24\xfc\xfc\x00" # 113=q - b"\x00\x7c\x7c\x04\x04\x0c\x08\x00" # 114=r - b"\x00\x48\x5c\x54\x54\x74\x20\x00" # 115=s - b"\x04\x04\x3f\x7f\x44\x64\x20\x00" # 116=t - b"\x00\x3c\x7c\x40\x40\x7c\x3c\x00" # 117=u - b"\x00\x1c\x3c\x60\x60\x3c\x1c\x00" # 118=v - b"\x00\x1c\x7c\x30\x18\x30\x7c\x1c" # 119=w - b"\x00\x44\x6c\x38\x38\x6c\x44\x00" # 120=x - b"\x00\x9c\xbc\xa0\xa0\xfc\x7c\x00" # 121=y - b"\x00\x44\x64\x74\x5c\x4c\x44\x00" # 122=z - b"\x00\x08\x08\x3e\x77\x41\x41\x00" # 123={ - b"\x00\x00\x00\xff\xff\x00\x00\x00" # 124=| - b"\x00\x41\x41\x77\x3e\x08\x08\x00" # 125=} - b"\x00\x02\x03\x01\x03\x02\x03\x01" # 126=~ - b"\xaa\x55\xaa\x55\xaa\x55\xaa\x55" # 127 -) - - -def _text(fb, s, x0, y0, c=1): - x = x0 - for ch in s: - code = ord(ch) - if code < 32 or code > 127: - code = 127 - base = (code - 32) * 8 - for j in range(8): - if 0 <= x < fb._width: - vline_data = _FONT_PETME128_8X8[base + j] - y = y0 - while vline_data: - if (vline_data & 1) and 0 <= y < fb._height: - fb._format.set_pixel(fb, x, y, c) - vline_data >>= 1 - y += 1 - x += 1 - - -class FrameBuffer: - """ - FrameBuffer object. - - Args: - buffer (bytearray): An object with a buffer protocol, large enough for - every pixel defined by width, height and format. - width (int): The width of the frame buffer in pixels. - height (int): The height of the frame buffer in pixels. - format (int): The format of the frame buffer. One of: - - ``MONO_VLSB``: Single bit displays (like SSD1306 OLED) - - ``MONO_HLSB``: Single bit files like PBM (Portable BitMap) - - ``MONO_HMSB``: Single bit displays where the bits in a byte are horizontally mapped - Each byte occupies 8 horizontal pixels with bit 0 being the leftmost. - - ``RGB565``: 16-bit color displays - - ``GS2_HMSB``: 2-bit color displays like the HT16K33 8x8 Matrix - - ``GS4_HMSB``: 4-bit grayscale displays - - ``GS8``: 8-bit grayscale/palette displays - stride (int): The number of pixels between each horizontal line of the frame buffer - If not given, it is assumed to be equal to the width. - """ - - def __init__(self, buffer, width, height, format, stride=None): - if width < 1 or height < 1: - raise ValueError("invalid width/height") - self._buffer = buffer - self._width = width - self._height = height - self._stride = stride if stride is not None else width - if format == MONO_VLSB: - self._format = MVLSBFormat() - elif format == MONO_HLSB: - self._stride = (self._stride + 7) & ~7 - self._format = MHLSBFormat() - elif format == MONO_HMSB: - self._stride = (self._stride + 7) & ~7 - self._format = MHMSBFormat() - elif format == RGB565: - self._format = RGB565Format() - elif format == GS2_HMSB: - self._stride = (self._stride + 3) & ~3 - self._format = GS2HMSBFormat() - elif format == GS4_HMSB: - self._stride = (self._stride + 1) & ~1 - self._format = GS4HMSBFormat() - elif format == GS8: - self._format = GS8Format() - else: - raise ValueError("invalid format") - - @property - def width(self): - """The width of the FrameBuffer in pixels.""" - return self._width - - @property - def height(self): - """The height of the FrameBuffer in pixels.""" - return self._height - - def fill(self, c): - """Fill the entire FrameBuffer with the specified color. - - Equivalent to ``fill_rect(0, 0, width, height, c)`` (matching - MicroPython, which has no separate per-format ``fill``). - """ - _fill_rect(self, 0, 0, self._width, self._height, c) - - def fill_rect(self, x, y, w, h, c): - """Draw a filled rectangle at the given location and size, in the given color.""" - _fill_rect(self, x, y, w, h, c) - - def pixel(self, x, y, c=None): - """ - Get or set the color of a given pixel. - - If ``c`` is not given, the color of the pixel is returned (or ``None`` if - ``x, y`` is out of bounds). If ``c`` is given, the pixel is set and - ``None`` is returned (a no-op if ``x, y`` is out of bounds). - """ - if x < 0 or x >= self._width or y < 0 or y >= self._height: - return None - if c is None: - return self._format.get_pixel(self, x, y) - self._format.set_pixel(self, x, y, c) - return None - - def hline(self, x, y, w, c): - """Draw a single pixel wide horizontal line.""" - _fill_rect(self, x, y, w, 1, c) - - def vline(self, x, y, h, c): - """Draw a single pixel wide vertical line.""" - _fill_rect(self, x, y, 1, h, c) - - def rect(self, x, y, w, h, c, f=False): - """Draw a rectangle at the given location, size and color. ``f`` fills it.""" - if f: - _fill_rect(self, x, y, w, h, c) - else: - _fill_rect(self, x, y, w, 1, c) - _fill_rect(self, x, y + h - 1, w, 1, c) - _fill_rect(self, x, y, 1, h, c) - _fill_rect(self, x + w - 1, y, 1, h, c) - - def line(self, x1, y1, x2, y2, c): - """Draw a single pixel wide line from (x1, y1) to (x2, y2).""" - _line(self, x1, y1, x2, y2, c) - - def ellipse(self, x, y, xr, yr, c, f=False, m=_ELLIPSE_MASK_ALL): - """ - Draw an ellipse centered at (x, y) with radii xr, yr. ``f`` fills it. - - ``m`` is a 4-bit quadrant mask (bit 0 = Q1 top-right, bit 1 = Q2 - top-left, bit 2 = Q3 bottom-left, bit 3 = Q4 bottom-right). - """ - _ellipse(self, x, y, xr, yr, c, f, m) - - def poly(self, x, y, coords, c, f=False): - """ - Draw a closed polygon at (x, y) using an array/list of coordinate pairs. - - ``coords`` may be an ``array('h', [x0, y0, x1, y1, ...])`` (as documented - for MicroPython) or a flat list/tuple of ints, or a list of (x, y) pairs. - ``f`` fills the polygon; otherwise only the outline is drawn. - """ - _poly(self, x, y, coords, c, f) - - def scroll(self, xstep, ystep): - """Shift the contents of the FrameBuffer by (xstep, ystep) pixels.""" - _scroll(self, xstep, ystep) - - def blit(self, source, x, y, key=-1, palette=None): - """Draw another FrameBuffer (or readonly tuple/list) on top of this one at (x, y).""" - _blit(self, source, x, y, key, palette) - - def text(self, s, x, y, c=1): - """ - Write text to the FrameBuffer using (x, y) as the upper-left corner. - - All characters are 8x8 pixels; there is no way to change the font. - """ - _text(self, s, x, y, c) - - -def FrameBuffer1(buffer, width, height, format, stride=None): - """Create a new FrameBuffer object. Here only for historical reasons.""" - return FrameBuffer(buffer, width, height, format, stride) diff --git a/src/add_ons/usdl2.py b/src/add_ons/usdl2.py deleted file mode 100644 index 27f0f0685..000000000 --- a/src/add_ons/usdl2.py +++ /dev/null @@ -1,926 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Brad Barnett -# -# SPDX-License-Identifier: MIT -""" -Pure-Python ``usdl2`` fallback: an **SDL2 subset for Python**. - -Used when the native C ``usdl2`` module is unavailable. Must stay in lockstep -with ``cmods/usdl2`` (see ``include/usdl2_module_globals.inc`` and -``src/usdl2_cpy.c``). - -Hard rule — **SDL2 symbols only**: - Export only names that exist in SDL2 (``SDL_*`` functions/constants/macros - and binding constructors for SDL types: ``SDL_Rect``, ``SDL_Point``, - ``SDL_Event``, ``SDL_TimerCallback``, ``SDL_DEFINE_PIXELFORMAT``, …). - Do **not** invent helpers such as ``process_exit``, ``pump_scheduler``, a - bare ``Event`` type, or other non-SDL module attributes. Past agents added - those for timing/shutdown; put such logic in the *consumer* (e.g. - ``sdldisplay``, ``multimer``) instead. MP cooperative timer delivery may - ride inside real SDL entry points (e.g. ``SDL_PumpEvents``) as a private - implementation detail — never as a new public name. - -Public surface matches native usdl2: same ``SDL_*`` API, 56-byte ``SDL_Event`` -with the same subviews, ``SDL_Rect``/``SDL_Point`` as packed bytes, timer API -via ``SDL_TimerCallback`` / ``SDL_AddTimer`` / ``SDL_RemoveTimer``. Opaque -handles are plain ints (falsy when 0/NULL). - -Shipped as ``add_ons/usdl2.py`` so MicroPython loads it via ``sys.path`` only -when the built-in/frozen ``usdl2`` module is not present. Uses **ctypes** on -CPython (unix and win32); **ffi** on MicroPython unix. -""" - -import struct -import sys - -try: - from micropython import const -except ImportError: - - def const(x): - return x - - -############################################################################### -# SDL2 Constants # -############################################################################### - -# SDL_WindowPos values -SDL_WINDOWPOS_UNDEFINED = const(0x1FFF0000) -SDL_WINDOWPOS_CENTERED = const(0x2FFF0000) - -# SDL_Window flags -SDL_WINDOW_FULLSCREEN = const(0x00000001) -SDL_WINDOW_OPENGL = const(0x00000002) -SDL_WINDOW_SHOWN = const(0x00000004) -SDL_WINDOW_HIDDEN = const(0x00000008) -SDL_WINDOW_BORDERLESS = const(0x00000010) -SDL_WINDOW_RESIZABLE = const(0x00000020) -SDL_WINDOW_MINIMIZED = const(0x00000040) -SDL_WINDOW_MAXIMIZED = const(0x00000080) -SDL_WINDOW_INPUT_GRABBED = const(0x00000100) -SDL_WINDOW_INPUT_FOCUS = const(0x00000200) -SDL_WINDOW_MOUSE_FOCUS = const(0x00000400) -SDL_WINDOW_FULLSCREEN_DESKTOP = const(0x00001001) -SDL_WINDOW_ALLOW_HIGHDPI = const(0x00002000) -SDL_WINDOW_MOUSE_CAPTURE = const(0x00004000) -SDL_WINDOW_ALWAYS_ON_TOP = const(0x00008000) -SDL_WINDOW_SKIP_TASKBAR = const(0x00010000) -SDL_WINDOW_UTILITY = const(0x00020000) -SDL_WINDOW_TOOLTIP = const(0x00040000) -SDL_WINDOW_POPUP_MENU = const(0x00080000) -SDL_WINDOW_VULKAN = const(0x10000000) - -# SDL_Renderer flags -SDL_RENDERER_SOFTWARE = const(0x00000001) -SDL_RENDERER_ACCELERATED = const(0x00000002) -SDL_RENDERER_PRESENTVSYNC = const(0x00000004) -SDL_RENDERER_TARGETTEXTURE = const(0x00000008) - -# SDL_Init flags -SDL_INIT_TIMER = const(0x00000001) -SDL_INIT_AUDIO = const(0x00000010) -SDL_INIT_VIDEO = const(0x00000020) -SDL_INIT_JOYSTICK = const(0x00000200) -SDL_INIT_HAPTIC = const(0x00001000) -SDL_INIT_GAMECONTROLLER = const(0x00002000) -SDL_INIT_EVENTS = const(0x00004000) -SDL_INIT_EVERYTHING = const(0x0000000F) -SDL_INIT_NOPARACHUTE = const(0x00100000) - -# SDL_Texture values -SDL_TEXTUREACCESS_STATIC = const(0) -SDL_TEXTUREACCESS_STREAMING = const(1) -SDL_TEXTUREACCESS_TARGET = const(2) - -# SDL_BlendMode values -SDL_BLENDMODE_NONE = const(1) -SDL_BLENDMODE_BLEND = const(2) -SDL_BLENDMODE_ADD = const(3) -SDL_BLENDMODE_MOD = const(4) -SDL_BLENDMODE_MUL = const(5) - -# SDL_Event types (not complete) -SDL_QUIT = const(0x100) # User clicked the window close button -SDL_KEYDOWN = const(0x300) # Key pressed -SDL_KEYUP = const(0x301) # Key released -SDL_MOUSEMOTION = const(0x400) # Mouse moved -SDL_MOUSEBUTTONDOWN = const(0x401) # Mouse button pressed -SDL_MOUSEBUTTONUP = const(0x402) # Mouse button released -SDL_MOUSEWHEEL = const(0x403) # Mouse wheel motion -SDL_FINGERDOWN = const(0x700) # Finger touched -SDL_FINGERUP = const(0x701) # Finger lifted -SDL_FINGERMOTION = const(0x702) # Finger moved -SDL_JOYAXISMOTION = const(0x600) # Joystick axis motion -SDL_JOYBALLMOTION = const(0x601) # Joystick trackball motion -SDL_JOYHATMOTION = const(0x602) # Joystick hat position change -SDL_JOYBUTTONDOWN = const(0x603) # Joystick button pressed -SDL_JOYBUTTONUP = const(0x604) # Joystick button released -SDL_JOYDEVICEADDED = const(0x605) # A joystick was connected -SDL_JOYDEVICEREMOVED = const(0x606) # A joystick was disconnected -SDL_POLLSENTINEL = const(0x7F00) # Signals the end of an event poll cycle - -# SDL_MouseMotionEvent button masks -SDL_BUTTON_LMASK = const(1 << 0) # Left mouse button -SDL_BUTTON_MMASK = const(1 << 1) # Middle mouse button -SDL_BUTTON_RMASK = const(1 << 2) # Right mouse button - -# SDL_JoyHatEvent position masks -SDL_HAT_CENTERED = const(0x00) -SDL_HAT_UP = const(0x01) -SDL_HAT_RIGHT = const(0x02) -SDL_HAT_DOWN = const(0x04) -SDL_HAT_LEFT = const(0x08) - - -############################################################################### -# SDL2 Pixel Formats # -############################################################################### - - -def SDL_DEFINE_PIXELFORMAT(type, order, layout, bits, bytes): - """ - Define a pixel format. - """ - return ( - (1 << 28) - | ((type) << 24) - | ((order) << 20) - | ((layout) << 16) - | ((bits) << 8) - | ((bytes) << 0) - ) - - -# SDL_PIXELTYPE values -SDL_PIXELTYPE_UNKNOWN = const(0) -SDL_PIXELTYPE_INDEX1 = const(1) -SDL_PIXELTYPE_INDEX4 = const(2) -SDL_PIXELTYPE_INDEX8 = const(3) -SDL_PIXELTYPE_PACKED8 = const(4) -SDL_PIXELTYPE_PACKED16 = const(5) -SDL_PIXELTYPE_PACKED32 = const(6) -SDL_PIXELTYPE_ARRAYU8 = const(7) -SDL_PIXELTYPE_ARRAYU16 = const(8) -SDL_PIXELTYPE_ARRAYU32 = const(9) -SDL_PIXELTYPE_ARRAYF16 = const(10) -SDL_PIXELTYPE_ARRAYF32 = const(11) - -# SDL_PACKEDORDER values -SDL_PACKEDORDER_NONE = const(0) -SDL_PACKEDORDER_XRGB = const(1) -SDL_PACKEDORDER_RGBX = const(2) -SDL_PACKEDORDER_ARGB = const(3) -SDL_PACKEDORDER_RGBA = const(4) -SDL_PACKEDORDER_XBGR = const(5) -SDL_PACKEDORDER_BGRX = const(6) -SDL_PACKEDORDER_ABGR = const(7) -SDL_PACKEDORDER_BGRA = const(8) - -# SDL_ARRAYORDER values -SDL_ARRAYORDER_NONE = const(0) -SDL_ARRAYORDER_RGB = const(1) -SDL_ARRAYORDER_RGBA = const(2) -SDL_ARRAYORDER_ARGB = const(3) -SDL_ARRAYORDER_BGR = const(4) -SDL_ARRAYORDER_BGRA = const(5) -SDL_ARRAYORDER_ABGR = const(6) - -# SDL_PACKEDLAYOUT values -SDL_PACKEDLAYOUT_NONE = const(0) -SDL_PACKEDLAYOUT_332 = const(1) -SDL_PACKEDLAYOUT_4444 = const(2) -SDL_PACKEDLAYOUT_1555 = const(3) -SDL_PACKEDLAYOUT_5551 = const(4) -SDL_PACKEDLAYOUT_565 = const(5) -SDL_PACKEDLAYOUT_8888 = const(6) -SDL_PACKEDLAYOUT_2101010 = const(7) -SDL_PACKEDLAYOUT_1010102 = const(8) - -# SDL_BITMAPORDER values -SDL_BITMAPORDER_NONE = const(0) -SDL_BITMAPORDER_4321 = const(1) -SDL_BITMAPORDER_1234 = const(2) - -# SDL_PIXELFORMAT values -SDL_PIXELFORMAT_UNKNOWN = const(0) -SDL_PIXELFORMAT_INDEX1LSB = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_4321, 0, 1, 0 -) -SDL_PIXELFORMAT_INDEX1MSB = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_1234, 0, 1, 0 -) -SDL_PIXELFORMAT_INDEX4LSB = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_4321, 0, 4, 0 -) -SDL_PIXELFORMAT_INDEX4MSB = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_1234, 0, 4, 0 -) -SDL_PIXELFORMAT_INDEX8 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX8, 0, 0, 8, 1) -SDL_PIXELFORMAT_RGB332 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED8, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_332, 8, 1 -) -SDL_PIXELFORMAT_XRGB4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_4444, 12, 2 -) -SDL_PIXELFORMAT_RGB444 = SDL_PIXELFORMAT_XRGB4444 -SDL_PIXELFORMAT_XBGR4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_4444, 12, 2 -) -SDL_PIXELFORMAT_BGR444 = SDL_PIXELFORMAT_XBGR4444 -SDL_PIXELFORMAT_XRGB1555 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_1555, 15, 2 -) -SDL_PIXELFORMAT_RGB555 = SDL_PIXELFORMAT_XRGB1555 -SDL_PIXELFORMAT_XBGR1555 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_1555, 15, 2 -) -SDL_PIXELFORMAT_BGR555 = SDL_PIXELFORMAT_XBGR1555 -SDL_PIXELFORMAT_ARGB4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_4444, 16, 2 -) -SDL_PIXELFORMAT_RGBA4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_4444, 16, 2 -) -SDL_PIXELFORMAT_ABGR4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_4444, 16, 2 -) -SDL_PIXELFORMAT_BGRA4444 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_4444, 16, 2 -) -SDL_PIXELFORMAT_ARGB1555 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_1555, 16, 2 -) -SDL_PIXELFORMAT_RGBA5551 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_5551, 16, 2 -) -SDL_PIXELFORMAT_ABGR1555 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_1555, 16, 2 -) -SDL_PIXELFORMAT_BGRA5551 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_5551, 16, 2 -) -SDL_PIXELFORMAT_RGB565 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_565, 16, 2 -) -SDL_PIXELFORMAT_BGR565 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_565, 16, 2 -) -SDL_PIXELFORMAT_RGB24 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_RGB, 0, 24, 3) -SDL_PIXELFORMAT_BGR24 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_BGR, 0, 24, 3) -SDL_PIXELFORMAT_XRGB8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_8888, 24, 4 -) -SDL_PIXELFORMAT_RGB888 = SDL_PIXELFORMAT_XRGB8888 -SDL_PIXELFORMAT_RGBX8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBX, SDL_PACKEDLAYOUT_8888, 24, 4 -) -SDL_PIXELFORMAT_XBGR8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_8888, 24, 4 -) -SDL_PIXELFORMAT_BGR888 = SDL_PIXELFORMAT_XBGR8888 -SDL_PIXELFORMAT_BGRX8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRX, SDL_PACKEDLAYOUT_8888, 24, 4 -) -SDL_PIXELFORMAT_ARGB8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_8888, 32, 4 -) -SDL_PIXELFORMAT_RGBA8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_8888, 32, 4 -) -SDL_PIXELFORMAT_ABGR8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_8888, 32, 4 -) -SDL_PIXELFORMAT_BGRA8888 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_8888, 32, 4 -) -SDL_PIXELFORMAT_ARGB2101010 = SDL_DEFINE_PIXELFORMAT( - SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_2101010, 32, 4 -) - - -############################################################################### -# SDL_Rect / SDL_Point # -############################################################################### - -# Packed as bytes (not a ctypes.Structure / uctypes struct) so the same value -# is usable, unmodified, on every backend -- matches py_SDL_Rect()/py_SDL_Point() -# in usdl2_cpy.c, which pack a bytes object directly. - - -def SDL_Rect(x=0, y=0, w=0, h=0): - return struct.pack(" a new zero-filled event. - * ``SDL_Event()`` -> a new event, copied from the buffer. - * ``SDL_Event()`` -> the same instance (identity). - """ - - def __new__(cls, event=None): - if isinstance(event, cls): - return event - self = super().__new__(cls) - if event is None: - self._data = bytearray(_EVENT_SIZE) - else: - buf = memoryview(event) - if len(buf) < _EVENT_SIZE: - raise ValueError("event buffer too small") - self._data = bytearray(buf[:_EVENT_SIZE]) - return self - - def __len__(self): - return _EVENT_SIZE - - @property - def type(self): - return _u32(self._data, 0) - - @type.setter - def type(self, value): - struct.pack_into(" "SDL_PollEvent" / "SDL_GetError".""" - if name.startswith("_lib_") or name.startswith("_raw_"): - return name[5:] - return name - - -def _bind_ffi(lib, specs): - for name, ret, args in specs: - globals()[name] = lib.func(ret, _libsym(name), args) - - -def _bind_ctypes(lib, specs): - for name, restype, argtypes in specs: - fn = getattr(lib, _libsym(name)) - fn.restype = restype - fn.argtypes = list(argtypes) - globals()[name] = fn - - -if _use_ffi: - _libSDL2 = ffi.open("libSDL2-2.0.so.0") - _bind_ffi(_libSDL2, _FFI_FUNCS) - _raw_SDL_GetError = globals()["_raw_SDL_GetError"] - _raw_SDL_GetKeyName = globals()["_raw_SDL_GetKeyName"] - - def _wrap_buf(buf): - return buf - - # modffi's "s" return type yields None for a NULL C string; SDL never - # actually returns NULL for either of these, but fall back to "" to - # match usdl2_cpy.c's PyUnicode_FromString(err ? err : "") exactly. - def SDL_GetError(): - err = _raw_SDL_GetError() - return err if err is not None else "" - - def SDL_GetKeyName(sym): - name = _raw_SDL_GetKeyName(sym) - return name if name is not None else "" - -else: - import ctypes - - if sys.platform == "win32": - _libSDL2 = ctypes.CDLL("SDL2.dll") - else: - _libSDL2 = ctypes.CDLL("libSDL2-2.0.so.0") - - _c = ctypes - _v = _c.c_void_p - _i = _c.c_int - _u = _c.c_uint - _d = _c.c_double - - # Rect/Point/Event/DisplayMode arguments are all c_void_p: SDL_Rect()/ - # SDL_Point() return plain bytes (accepted directly by ctypes for a - # c_void_p argument), and _wrap_buf() below adapts writable bytearrays - # (SDL_Event, out-params) the same way usdl2_cpy.c's PyObject_GetBuffer() - # accepts any bytes-like object. - _CTYPES_FUNCS = ( - ("SDL_Init", _i, (_u,)), - ("SDL_InitSubSystem", _i, (_u,)), - ("SDL_Quit", None, ()), - ("_raw_SDL_GetError", _c.c_char_p, ()), - ("_raw_SDL_CreateWindow", _v, (_c.c_char_p, _i, _i, _i, _i, _u)), - ("SDL_DestroyWindow", None, (_v,)), - ("SDL_SetWindowSize", None, (_v, _i, _i)), - ("SDL_SetWindowResizable", None, (_v, _i)), - ("SDL_SetWindowMinimumSize", None, (_v, _i, _i)), - ("SDL_SetWindowMaximumSize", None, (_v, _i, _i)), - ("SDL_CreateRenderer", _v, (_v, _i, _u)), - ("SDL_DestroyRenderer", None, (_v,)), - ("SDL_SetRenderDrawColor", _i, (_v, _u, _u, _u, _u)), - ("SDL_SetRenderTarget", _i, (_v, _v)), - ("SDL_RenderClear", _i, (_v,)), - ("SDL_RenderCopy", _i, (_v, _v, _v, _v)), - ("SDL_RenderCopyEx", _i, (_v, _v, _v, _v, _d, _v, _i)), - ("SDL_RenderPresent", None, (_v,)), - ("SDL_RenderFillRect", _i, (_v, _v)), - ("SDL_RenderSetLogicalSize", _i, (_v, _i, _i)), - ("SDL_CreateTexture", _v, (_v, _u, _i, _i, _i)), - ("SDL_DestroyTexture", None, (_v,)), - ("SDL_SetTextureBlendMode", _i, (_v, _i)), - ("SDL_NumJoysticks", _i, ()), - ("SDL_JoystickOpen", _v, (_i,)), - ("SDL_JoystickClose", None, (_v,)), - ("SDL_JoystickInstanceID", _i, (_v,)), - ("_raw_SDL_GetKeyName", _c.c_char_p, (_i,)), - ("_lib_SDL_PumpEvents", None, ()), - ("_lib_SDL_PollEvent", _i, (_v,)), - ("_lib_SDL_UpdateTexture", _i, (_v, _v, _v, _i)), - ("_lib_SDL_GetDisplayUsableBounds", _i, (_i, _v)), - ("_lib_SDL_GetDesktopDisplayMode", _i, (_i, _v)), - # SDL_AddTimer/SDL_RemoveTimer are bound separately below (need the - # timer trampoline's CFUNCTYPE to exist first). - ) - _bind_ctypes(_libSDL2, _CTYPES_FUNCS) - _raw_SDL_GetError = globals()["_raw_SDL_GetError"] - _raw_SDL_GetKeyName = globals()["_raw_SDL_GetKeyName"] - _raw_SDL_CreateWindow = globals()["_raw_SDL_CreateWindow"] - - def _wrap_buf(buf): - """Adapt a bytes-like object for a ctypes c_void_p argument. - - Immutable ``bytes`` (e.g. from SDL_Rect()) already convert directly; - anything else supporting the buffer protocol (bytearray, memoryview, - array.array, ...) is wrapped with from_buffer() so the callee can - read/write through the same memory in place, matching usdl2_cpy.c's - PyObject_GetBuffer() flexibility. - """ - if buf is None or isinstance(buf, (bytes, int, ctypes.Array)): - return buf - return (ctypes.c_char * len(buf)).from_buffer(buf) - - # SDL_GetError()/SDL_GetKeyName() use c_char_p (not modelled as "P" in the - # ffi table above) so ctypes decodes the returned C string for us; wrap - # them to fall back to "" for NULL, matching usdl2_cpy.c. - def SDL_GetError(): - err = _raw_SDL_GetError() - return err.decode("utf-8") if err else "" - - def SDL_GetKeyName(sym): - name = _raw_SDL_GetKeyName(sym) - return name.decode("utf-8") if name else "" - - def SDL_CreateWindow(title, x, y, w, h, flags): - if isinstance(title, str): - title = title.encode("utf-8") - return _raw_SDL_CreateWindow(title, x, y, w, h, flags) - - -# _bind_ffi / _bind_ctypes assign these via globals()[name]; materialize so -# static analysis and the buffer wrappers below see real module bindings. -_lib_SDL_PumpEvents = globals()["_lib_SDL_PumpEvents"] -_lib_SDL_PollEvent = globals()["_lib_SDL_PollEvent"] -_lib_SDL_UpdateTexture = globals()["_lib_SDL_UpdateTexture"] -_lib_SDL_GetDisplayUsableBounds = globals()["_lib_SDL_GetDisplayUsableBounds"] -_lib_SDL_GetDesktopDisplayMode = globals()["_lib_SDL_GetDesktopDisplayMode"] - - -############################################################################### -# Timer API # -############################################################################### - - -class _TimerCallback: - """Opaque token returned by SDL_TimerCallback(); only usable via SDL_AddTimer().""" - - __slots__ = ("callback",) - - def __init__(self, callback): - if not callable(callback): - raise TypeError("callback must be callable") - self.callback = callback - - -def SDL_TimerCallback(callback): - return _TimerCallback(callback) - - -if _use_ffi: - # Real SDL timers fire on an SDL-owned pthread that MicroPython's runtime - # never registered (mp_thread_init() was never called for it); invoking - # any Python callback from that thread segfaults unconditionally -- - # verified experimentally, even a trivial ffi.callback(..., lock=True) - # trampoline crashes the moment SDL's timer thread calls it. So on - # MicroPython, timers are cooperative/software instead of real SDL ones: - # SDL_AddTimer() just records a deadline, and SDL_PumpEvents()/ - # SDL_PollEvent() -- already polled regularly by pydisplay's event loop, - # on a safe/registered thread -- fire any due callbacks in-line. - import time - - _sw_timers = {} - _sw_timer_next_id = [1] - - def _sw_timers_poll(): - if not _sw_timers: - return - now = time.ticks_ms() - for timer_id, entry in list(_sw_timers.items()): - deadline, interval, callback, user_param = entry - if time.ticks_diff(now, deadline) < 0: - continue - if timer_id not in _sw_timers: - continue - _sw_timers[timer_id] = ( - time.ticks_add(deadline, interval), - interval, - callback, - user_param, - ) - try: - callback(interval, user_param) - except Exception: - pass - - def SDL_AddTimer(interval, tcb, user_param): - if not isinstance(tcb, _TimerCallback): - raise TypeError("callback must be from SDL_TimerCallback()") - timer_id = _sw_timer_next_id[0] - _sw_timer_next_id[0] += 1 - _sw_timers[timer_id] = ( - time.ticks_add(time.ticks_ms(), interval), - interval, - tcb.callback, - user_param, - ) - return timer_id - - def SDL_RemoveTimer(timer): - return 1 if _sw_timers.pop(timer, None) is not None else 0 - -else: - _TIMER_MAX = const(8) - _timer_slots = [None] * _TIMER_MAX # (callback, user_param, ret_interval) or None - _timer_id_to_slot = {} - - def _sw_timers_poll(): - pass # ctypes timers run for real via SDL's own thread; nothing to poll. - - def _timer_trampoline(interval, slot): - """ - Runs on an SDL-owned thread, but ctypes callbacks always re-acquire the - GIL for us (PyGILState_Ensure()/Release() around every callback - invocation), so this is safe unlike the MicroPython ffi case above. - Looks the entry up through the slot table (not a raw pointer) so a - concurrent SDL_RemoveTimer() cannot use freed state; mirrors - timer_trampoline() in usdl2_cpy.c. The next interval is always the one - the timer was created with (not the callback's return value). - """ - entry = _timer_slots[slot] if 0 <= slot < _TIMER_MAX else None - if entry is None: - return 0 - callback, user_param, ret_interval = entry - try: - callback(interval, user_param) - except Exception: - pass - return ret_interval - - _sdl_timer_functype = ctypes.CFUNCTYPE(ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t) - _sdl_timer_cfunc = _sdl_timer_functype(_timer_trampoline) - _lib_SDL_AddTimer = _libSDL2.SDL_AddTimer - _lib_SDL_AddTimer.restype = ctypes.c_int - _lib_SDL_AddTimer.argtypes = (ctypes.c_uint32, _sdl_timer_functype, ctypes.c_size_t) - _lib_SDL_RemoveTimer = _libSDL2.SDL_RemoveTimer - _lib_SDL_RemoveTimer.restype = ctypes.c_int - _lib_SDL_RemoveTimer.argtypes = (ctypes.c_int,) - - def SDL_AddTimer(interval, tcb, user_param): - if not isinstance(tcb, _TimerCallback): - raise TypeError("callback must be from SDL_TimerCallback()") - slot = -1 - for i in range(_TIMER_MAX): - if _timer_slots[i] is None: - slot = i - break - if slot < 0: - raise RuntimeError("too many SDL timers") - _timer_slots[slot] = (tcb.callback, user_param, interval) - timer_id = _lib_SDL_AddTimer(interval, _sdl_timer_cfunc, slot) - if not timer_id: - _timer_slots[slot] = None - return 0 - _timer_id_to_slot[timer_id] = slot - return timer_id - - def SDL_RemoveTimer(timer): - slot = _timer_id_to_slot.pop(timer, None) - if slot is not None: - _timer_slots[slot] = None - return 1 if _lib_SDL_RemoveTimer(timer) else 0 - - -############################################################################### -# Buffer-taking wrappers # -############################################################################### - -# Defined once, backend-independent: _wrap_buf() and the private _lib_SDL_* -# names above are already backend-specific; the logic here mirrors -# usdl2_cpy.c's Python-facing signatures exactly. SDL_PumpEvents()/ -# SDL_PollEvent() also drive the MicroPython software-timer poll above (a -# no-op on the ctypes backend, where real SDL timers do the work). - - -def SDL_PumpEvents(): - _lib_SDL_PumpEvents() - _sw_timers_poll() - - -def SDL_PollEvent(event): - _sw_timers_poll() - data = event._data if isinstance(event, SDL_Event) else event - return bool(_lib_SDL_PollEvent(_wrap_buf(data))) - - -def SDL_UpdateTexture(texture, rect, pixels, pitch): - return _lib_SDL_UpdateTexture(texture, _wrap_buf(rect), _wrap_buf(pixels), pitch) - - -def SDL_GetDisplayUsableBounds(display_index, rect=None): - return _lib_SDL_GetDisplayUsableBounds(display_index, _wrap_buf(rect)) - - -def SDL_GetDesktopDisplayMode(display_index, mode=None): - return _lib_SDL_GetDesktopDisplayMode(display_index, _wrap_buf(mode)) diff --git a/src/examples/roku_remote/roku_graphics.py b/src/examples/roku_remote/roku_graphics.py index 7a557eb38..a167e03bb 100644 --- a/src/examples/roku_remote/roku_graphics.py +++ b/src/examples/roku_remote/roku_graphics.py @@ -13,7 +13,7 @@ and is what ``roku_remote`` launches. Shares :class:`roku_engine.RokuEngine` for ECP over the LAN (SSDP discover + -HTTP keypress / apps / queries). Uses only the four core pydisplay packages +HTTP keypress / apps / queries). Uses only the three core pydisplay packages (``graphics``, ``displaysys`` via ``board_config``, ``eventsys``, ``multimer``). Geometry scales from a 320x480 reference up through tall phone portraits. diff --git a/src/examples/tower_climb/tower_climb.py b/src/examples/tower_climb/tower_climb.py index 9d3d90f31..b6cf9ce30 100644 --- a/src/examples/tower_climb/tower_climb.py +++ b/src/examples/tower_climb/tower_climb.py @@ -7,7 +7,7 @@ hardware ``vscsad`` / rotation tricks) so it runs on PGDisplay and SDLDisplay. Layout scales from the 320×480 reference to taller panels (480×800, 720×720, …). -Built from the four core pydisplay libraries only. +Built from the three core pydisplay libraries only. """ import sys diff --git a/src/lib/displaysys/README.md b/src/lib/displaysys/README.md index b188e12eb..d8e32cbe1 100644 --- a/src/lib/displaysys/README.md +++ b/src/lib/displaysys/README.md @@ -13,7 +13,7 @@ pip install \ displaysys ``` -For desktop SDL, also install `usdl2` (same two-index pattern). For PyGame, install `pygame-ce` from PyPI (`import pygame`). +For desktop SDL, also install native `usdl2` (or pure-Python `usdl2-py`) with the same two-index pattern. For PyGame, install `pygame-ce` from PyPI (`import pygame`). ### MicroPython (MIP) @@ -47,7 +47,7 @@ Desktop input backends use [eventsys](https://test.pypi.org/project/eventsys/) a - [Documentation — Displays](https://pydisplay.readthedocs.io/en/latest/concepts/displays/) - [Source](https://github.com/PyDevices/pydisplay) - [Issues](https://github.com/PyDevices/pydisplay/issues) -- Related: [eventsys](https://test.pypi.org/project/eventsys/), [multimer](https://test.pypi.org/project/multimer/), [pydisplay-graphics](https://test.pypi.org/project/pydisplay-graphics/), [usdl2](https://test.pypi.org/project/usdl2/) +- Related: [eventsys](https://test.pypi.org/project/eventsys/), [multimer](https://test.pypi.org/project/multimer/), [graphics-py](https://test.pypi.org/project/graphics-py/), [usdl2](https://test.pypi.org/project/usdl2/), [usdl2-py](https://test.pypi.org/project/usdl2-py/) ## License diff --git a/src/lib/graphics/README.md b/src/lib/graphics/README.md deleted file mode 100644 index f5720dcb0..000000000 --- a/src/lib/graphics/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# pydisplay-graphics - -Pure-Python `graphics` package for pydisplay — `FrameBuffer`, `Draw`, fonts, shapes, and image loaders. Import as `graphics`. - -> **Pip name:** `pydisplay-graphics` · **Import:** `import graphics` - -On desktop/Android when a native wheel is available, prefer [`graphics-cmod`](https://test.pypi.org/project/graphics-cmod/) (same import name, C implementation). - -## Install - -### CPython (TestPyPI) - -```bash -pip install \ - -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - pydisplay-graphics -``` - -### MicroPython (MIP) - -```python -import mip -mip.install("graphics", index="https://PyDevices.github.io/micropython-lib/mip/PyDevices") -``` - -## Quick start - -```python -import graphics - -fb = graphics.FrameBuffer( - bytearray(160 * 128 * 2), 160, 128, graphics.RGB565 -) -fb.fill(0) -fb.fill_rect(10, 10, 40, 40, 0xF800) -print(graphics.implementation()) # e.g. pydisplay_python -``` - -Higher-level drawing: - -```python -from graphics import Draw, Area - -draw = Draw(fb) -draw.rect(0, 0, 50, 50, 0x07E0) -draw.text8("hi", 2, 2, 0xFFFF) -``` - -## What you get - -- `FrameBuffer` — framebuf-compatible surface with shapes, blit, and Area bounds -- `Draw` — clip stack and text helpers over a canvas -- Fonts (`text8`, `text14`, `text16`, `Font`) and loaders (`BMP565`, PBM/PGM/BMP helpers) -- `Area` geometry and module-level shape helpers - -## Links - -- [Documentation — graphics](https://pydisplay.readthedocs.io/en/latest/concepts/graphics/) -- [Documentation — Drawing and fonts](https://pydisplay.readthedocs.io/en/latest/concepts/drawing-and-fonts/) -- [Source](https://github.com/PyDevices/pydisplay) -- [Issues](https://github.com/PyDevices/pydisplay/issues) -- Related: [graphics-cmod](https://test.pypi.org/project/graphics-cmod/), [displaysys](https://test.pypi.org/project/displaysys/) - -## License - -MIT — see [LICENSE](https://github.com/PyDevices/pydisplay/blob/main/LICENSE). diff --git a/src/lib/graphics/__init__.py b/src/lib/graphics/__init__.py deleted file mode 100644 index aa155c7ed..000000000 --- a/src/lib/graphics/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -graphics — cross-platform 2D drawing for *Python. - -Extends MicroPython's ``framebuf`` with shape helpers, fonts, image loaders, and -``Area`` bounding boxes for partial updates. On CPython and CircuitPython the -built-in pure-Python ``framebuf`` fallback is used automatically. - -Quick start:: - - import graphics - - fb = graphics.FrameBuffer(bytearray(16 * 16 * 2), 16, 16, graphics.RGB565) - fb.fill(0) - area = fb.fill_rect(1, 1, 6, 6, 0xFFFF) - graphics.text8(fb, "Hi", 0, 0, 0xFFFF) -""" - -from ._area import Area -from ._bmp565 import BMP565 -from ._clip import ClipContext, ClippedCanvas -from ._draw import Draw -from ._files import ( - bmp_to_framebuffer, - export_framebuffer, - load_image, - pbm_to_framebuffer, - pgm_to_framebuffer, - save_image, -) -from ._font import Font, text, text8, text14, text16 -from ._framebuf_plus import ( - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_HMSB, - MONO_VLSB, - RGB565, - RGB888, - FrameBuffer, -) -from ._shapes import ( - arc, - blit, - blit_rect, - blit_transparent, - circle, - ellipse, - fill, - fill_rect, - gradient_rect, - hline, - line, - pixel, - poly, - polygon, - rect, - round_rect, - triangle, - vline, -) - - -def implementation(): - """Return ``pydisplay_python`` (this package) vs ``native_cmod`` on embedded builds.""" - return "pydisplay_python" - - -__all__ = [ - "BMP565", - "GS2_HMSB", - "GS4_HMSB", - "GS8", - "MONO_HLSB", - "MONO_HMSB", - "MONO_VLSB", - "RGB565", - "Area", - "ClipContext", - "ClippedCanvas", - "Draw", - "Font", - "FrameBuffer", - "arc", - "blit", - "blit_rect", - "blit_transparent", - "bmp_to_framebuffer", - "circle", - "ellipse", - "export_framebuffer", - "fill", - "fill_rect", - "gradient_rect", - "hline", - "implementation", - "line", - "load_image", - "pbm_to_framebuffer", - "pgm_to_framebuffer", - "pixel", - "poly", - "polygon", - "rect", - "round_rect", - "save_image", - "text", - "text8", - "text14", - "text16", - "triangle", - "vline", -] diff --git a/src/lib/graphics/_area.py b/src/lib/graphics/_area.py deleted file mode 100644 index e5a4ecd44..000000000 --- a/src/lib/graphics/_area.py +++ /dev/null @@ -1,270 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Brad Barnett -# -# SPDX-License-Identifier: MIT -""" -`graphics._area` -==================================================== -Area class for defining rectangular areas. -""" - - -class Area: - """ - Represents a rectangular area defined by its position and dimensions. - - Attributes: - x (int | float): The x-coordinate of the top-left corner of the area. - y (int | float): The y-coordinate of the top-left corner of the area. - w (int | float): The width of the area. - h (int | float): The height of the area. - - Methods: - contains(x, y): Checks if the specified point is contained within the area. - contains_area(other): Checks if the specified area is contained within the area. - intersects(other): Checks if the current Area object intersects with another Area object. - touches_or_intersects(other): Checks if the current Area object touches or intersects with another Area object. - shift(dx=0, dy=0): Returns a new Area shifted by the specified amount in the x and y directions. - clip(other): Clips the current Area object to the specified Area object. - - Special Methods: - __eq__(other): Checks if the current Area object is equal to another Area object. - __ne__(other): Checks if the current Area object is not equal to another Area object. - __add__(other): Computes the union of the current Area object and another Area object. - __iter__(): Returns an iterator over the elements of the Area object. - __repr__(): Returns a string representation of the Area object. - __str__(): Returns a string representation of the Area object. - """ - - __hash__ = None - - def __init__(self, x, y=None, w=None, h=None): - """ - Initializes a new instance of the Area class. - - Args: - x (int | float | tuple): The x-coordinate of the top-left corner of the area or - a tuple containing the x, y, w, and h coordinates of the area. - y (int | float): The y-coordinate of the top-left corner of the area. - w (int | float): The width of the area. - h (int | float): The height of the area. - """ - if isinstance(x, tuple): - x, y, w, h = x - if y is None or w is None or h is None: - raise ValueError("Invalid arguments") - self.x = x - self.y = y - self.w = w - self.h = h - - def contains(self, x, y=None): - """ - Checks if the specified point is contained within the area. - - Args: - x (int | tuple): The x-coordinate of the point to check - or a tuple containing the x and y coordinates of the point. - y (int): The y-coordinate of the point to check. - - Returns: - (bool): True if the point is contained within the area, False otherwise. - """ - if isinstance(x, tuple): - x, y = x - return self.x <= x < self.x + self.w and self.y <= y < self.y + self.h - - def contains_area(self, other): - """ - Checks if the specified area is contained within the area. - - Args: - other (Area): The other area to check. - - Returns: - (bool): True if the other area is contained within the area, False otherwise. - """ - return ( - self.x <= other.x - and self.y <= other.y - and self.x + self.w >= other.x + other.w - and self.y + self.h >= other.y + other.h - ) - - def intersects(self, other): - """ - Checks if the current Area object intersects with another Area object. - - Args: - other (Area): The other Area object to check for overlap. - - Returns: - (bool): True if the two Area objects intersect, False otherwise. - """ - if self.x + self.w <= other.x or other.x + other.w <= self.x: - return False - return not (self.y + self.h <= other.y or other.y + other.h <= self.y) - - def touches_or_intersects(self, other): - """ - Checks if the current Area object touches or intersects with another Area object. - - Args: - other (Area): The other Area object to check for overlap or touch. - - Returns: - (bool): True if the two Area objects touch or intersect, False otherwise. - """ - if self.x + self.w < other.x or other.x + other.w < self.x: - return False - return not (self.y + self.h < other.y or other.y + other.h < self.y) - - def shift(self, dx=0, dy=0): - """ - Returns a new Area shifted by the specified amount in the x and y directions. - - Args: - dx (int | float): The amount to shift the area in the x direction. - dy (int | float): The amount to shift the area in the y direction. - - Returns: - (Area): A new Area object shift by the specified amount in the x and y directions. - """ - return Area(self.x + dx, self.y + dy, self.w, self.h) - - def clip(self, other): - """ - Clips the current Area object to the specified Area object. - - Args: - other (Area): The other Area object to clip to. - - Returns: - (Area): A new Area object representing the clipped area. - """ - x = max(self.x, other.x) - y = max(self.y, other.y) - w = min(self.x + self.w, other.x + other.w) - x - h = min(self.y + self.h, other.y + other.h) - y - return Area(x, y, w, h) - - def offset(self, d1, d2=None, d3=None, d4=None): - """ - Returns a new Area offset by the specified amount(s). - - If only one argument is provided, it is used as the offset in all 4 directions. - If two arguments are provided, the first is used as the offset in the x direction and the second as the offset in the y direction. - If three arguments are provided, they are used as the offsets in the left, top/bottom, and right directions, respectively. - If four arguments are provided, they are used as the offsets in the left, top, right, and bottom directions, respectively. - - Args: - d1 (int | float): The offset in the x direction or the offset in all 4 directions. - d2 (int | float): The offset in the y direction or the offset in the top/bottom direction. - d3 (int | float): The offset in the right direction. - d4 (int | float): The offset in the bottom direction. - - Returns: - (Area): A new Area object offset by the specified amount(s). - """ - if d2 is None: - d2 = d3 = d4 = d1 - elif d3 is None: - d3 = d1 - d4 = d2 - elif d4 is None: - d4 = d2 - return Area(self.x - d1, self.y - d2, self.w + d1 + d3, self.h + d2 + d4) - - def inset(self, d1, d2=None, d3=None, d4=None): - """ - Returns a new Area inset by the specified amount(s). - - If only one argument is provided, it is used as the inset in all 4 directions. - If two arguments are provided, the first is used as the inset in the x direction and the second as the inset in the y direction. - If three arguments are provided, they are used as the insets in the left, top/bottom, and right directions, respectively. - If four arguments are provided, they are used as the insets in the left, top, right, and bottom directions, respectively. - - Args: - d1 (int | float): The inset in the x direction or the inset in all 4 directions. - d2 (int | float): The inset in the y direction or the inset in the top/bottom direction. - d3 (int | float): The inset in the right direction. - d4 (int | float): The inset in the bottom direction. - - Returns: - (Area): A new Area object inset by the specified amount(s). - """ - if d2 is None: - d2 = d3 = d4 = d1 - elif d3 is None: - d3 = d1 - d4 = d2 - elif d4 is None: - d4 = d2 - return Area(self.x + d1, self.y + d2, self.w - d1 - d3, self.h - d2 - d4) - - def __eq__(self, other): - """ - Checks if the current Area object is equal to another Area object. - - Args: - other (Area): The other Area object to compare with. - - Returns: - (bool): True if the two Area objects are equal, False otherwise. - """ - return self.x == other.x and self.y == other.y and self.w == other.w and self.h == other.h - - def __ne__(self, other): - """ - Checks if the current Area object is not equal to another Area object. - - Args: - other (Area): The other Area object to compare with. - - Returns: - (bool): True if the two Area objects are not equal, False otherwise. - """ - return not self.__eq__(other) - - def __add__(self, other): - """ - Computes the union of the current Area object and another Area object. - - Args: - other (Area): The other Area object to compute the union with. - - Returns: - (Area): A new Area object representing the union of the two areas. - """ - return Area( - min(self.x, other.x), - min(self.y, other.y), - max(self.x + self.w, other.x + other.w) - min(self.x, other.x), - max(self.y + self.h, other.y + other.h) - min(self.y, other.y), - ) - - def __iter__(self): - """ - Returns an iterator over the elements of the Area object. - - Returns: - (iterator): An iterator over the elements of the Area object. - """ - return iter((self.x, self.y, self.w, self.h)) - - def __repr__(self): - """ - Returns a string representation of the Area object. - - Returns: - (str): A string representation of the Area object. - """ - return f"Area({self.x}, {self.y}, {self.w}, {self.h})" - - def __str__(self): - """ - Returns a string representation of the Area object. - - Returns: - (str): A string representation of the Area object. - """ - return f"Area({self.x}, {self.y}, {self.w}, {self.h})" diff --git a/src/lib/graphics/_blit_hooks.py b/src/lib/graphics/_blit_hooks.py deleted file mode 100644 index 6f38e4536..000000000 --- a/src/lib/graphics/_blit_hooks.py +++ /dev/null @@ -1,131 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Blit helpers for display-driver fast paths and clipping.""" - -from ._area import Area - -_RGB565_BPP = 2 - - -def clip_blit_bounds(canvas, source, x, y): - """Return clipped destination/source origin as ``(x0, y0, w, h, src_x, src_y)``. - - Returns ``None`` when the blit is fully outside the canvas. - """ - if ( - (-x >= source.width) - or (-y >= source.height) - or (x >= canvas.width) - or (y >= canvas.height) - ): - return None - - x0 = max(0, x) - y0 = max(0, y) - src_x = max(0, -x) - src_y = max(0, -y) - w = min(canvas.width, x + source.width) - x0 - h = min(canvas.height, y + source.height) - y0 - return x0, y0, w, h, src_x, src_y - - -def canvas_accepts_blit_rect(canvas): - """True when ``canvas.blit_rect`` is a display-style fast path (not RAM copy).""" - if not callable(getattr(canvas, "blit_rect", None)): - return False - from ._framebuf_plus import FrameBuffer - - return not isinstance(canvas, FrameBuffer) - - -def canvas_accepts_blit_transparent(canvas): - """True when ``canvas.blit_transparent`` is implemented by the display driver.""" - if not callable(getattr(canvas, "blit_transparent", None)): - return False - from ._framebuf_plus import FrameBuffer - - return not isinstance(canvas, FrameBuffer) - - -def _source_rgb565_bytes_per_pixel(source): - fmt = getattr(source, "format", None) - if fmt is None: - return None - from ._framebuf_plus import RGB565 - - return _RGB565_BPP if fmt == RGB565 else None - - -def _framebuffer_base_blit(canvas, source, x, y, key=-1, palette=None): - """Call ``framebuf.FrameBuffer.blit`` without re-entering graphics overrides.""" - base = type(canvas).__mro__[1] - base.blit(canvas, source, x, y, key, palette) - - -def _extract_rgb565_rows(source, src_x, src_y, w, h): - bpp = _RGB565_BPP - row_bytes = w * bpp - out = bytearray(row_bytes * h) - for row in range(h): - src_start = ((src_y + row) * source.width + src_x) * bpp - src_end = src_start + row_bytes - dst_start = row * row_bytes - out[dst_start : dst_start + row_bytes] = source.buffer[src_start:src_end] - return out - - -def blit_rect_to_buffer(canvas, buf, x, y, w, h, *, bpp=_RGB565_BPP): - """Copy an RGB565 rectangle into ``canvas.buffer``.""" - if x < 0 or y < 0 or x + w > canvas.width or y + h > canvas.height: - raise ValueError("The provided x, y, w, h values are out of range") - - expected = w * h * bpp - if len(buf) != expected: - raise ValueError( - f"The source buffer is not the correct size (got {len(buf)} bytes, expected {expected})" - ) - - for row in range(h): - source_begin = row * w * bpp - source_end = source_begin + w * bpp - dest_begin = ((y + row) * canvas.width + x) * bpp - dest_end = dest_begin + w * bpp - canvas.buffer[dest_begin:dest_end] = buf[source_begin:source_end] - - -def blit_rect_dispatch(canvas, buf, x, y, w, h): - """Blit a raw RGB565 buffer, using a canvas hook when available.""" - if canvas_accepts_blit_rect(canvas): - canvas.blit_rect(buf, x, y, w, h) - else: - blit_rect_to_buffer(canvas, buf, x, y, w, h) - return Area(x, y, w, h) - - -def try_fast_framebuffer_blit(canvas, source, x, y, key=-1, palette=None): - """Use framebuffer or display fast paths when available. - - Returns an :class:`Area` on success, or ``None`` to use the pixel loop. - """ - clipped = clip_blit_bounds(canvas, source, x, y) - if clipped is None: - return None - - x0, y0, w, h, src_x, src_y = clipped - - from ._framebuf_plus import FrameBuffer - - if isinstance(canvas, FrameBuffer) and isinstance(source, FrameBuffer): - _framebuffer_base_blit(canvas, source, x, y, key, palette) - return Area(x0, y0, w, h) - - if key != -1 or palette is not None: - return None - - if _source_rgb565_bytes_per_pixel(source) is None or not hasattr(source, "buffer"): - return None - - data = _extract_rgb565_rows(source, src_x, src_y, w, h) - blit_rect_dispatch(canvas, data, x0, y0, w, h) - return Area(x0, y0, w, h) diff --git a/src/lib/graphics/_bmp565.py b/src/lib/graphics/_bmp565.py deleted file mode 100644 index 852feba9c..000000000 --- a/src/lib/graphics/_bmp565.py +++ /dev/null @@ -1,199 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""RGB565 Windows BMP read/write helpers and sliceable BMP565 asset.""" - -import os -import struct - -BMP565_BPP = 16 -BMP565_BYTES_PER_PIXEL = 2 - - -def read_bmp565_header(f): - """Read an RGB565 BMP header from an open binary file. - - Returns: - tuple[int, int, int]: width, height, data_offset - """ - if f.read(2) != b"BM": - raise ValueError("Not a BMP file") - f.seek(10) - data_offset = struct.unpack("= canvas.width or \ - # y < -self.height or y >= canvas.height: - # return - # Go through each row of the character. - for char_y in range(self._font_height): - # Grab the byte for the current row of font data. - if not (line := self._read_line(char, char_y)): - continue # maybe character isnt there? go to next - # Go through each column in the row byte. - for char_x in range(self.width): - # Draw a pixel for each bit that's flipped on. - if (line >> (self.width - char_x - 1)) & 0x1: - canvas.fill_rect( - ( - x + char_x * scale - if not inverted - else x + (self._font_width - char_x - 1) * scale - ), - ( - y + char_y * scale - if not inverted - else y + (self._font_height - char_y - 1) * scale - ), - scale, - scale, - color, - ) - return Area(x, y, self._font_width * scale, self._font_height * scale) - - def text(self, canvas, string, x, y, color, scale=1, inverted=False): - """ - Draw text to the canvas. - - Args: - canvas (Canvas): The DisplayDriver, FrameBuffer, or other canvas-like object to draw on. - string (str): The text to draw. - x (int): The x position to start drawing the text. - y (int): The y position to start drawing the text. - color (int): The color to draw the text in. - scale (int): The scale factor to draw the text at. Default is 1. - inverted (bool): If True, draw the text inverted. Default is False. - - Returns: - (Area): The area that was drawn to. - """ - if inverted: - string = "".join(reversed(string)) - - char_y = y - largest_x = 0 # the last x position reached on the longest line - for chunk in string.split("\n"): - last_x = x # the last x position reached on the current line - for i, char in enumerate(chunk): - char_x = x + (i * self.width * scale) - if ( - (char_x < canvas.width if hasattr(canvas, "width") else True) - and (char_y < canvas.height if hasattr(canvas, "height") else True) - and char_x + (self.width * scale) > 0 - and char_y + (self.height * scale) > 0 - ): - self.draw_char( - char, - char_x, - char_y, - canvas, - color, - scale=scale, - inverted=inverted, - ) - last_x = char_x + (self.width * scale) - largest_x = max([largest_x, last_x]) # update the largest x position - char_y += self.height * scale - return Area(x, y, largest_x - x, char_y - y) - - def text_width(self, text, scale=1): - """ - Return the pixel width of the specified text message. - Takes into account the scale factor, but not any newlines. - - Args: - text (str): The text to measure. - scale (int): The scale factor to measure the text at. Default is 1. - """ - return len(text) * self._font_width * scale - - def _read_line(self, char, line): - """Read a line of font data for a character.""" - # ROM fonts cover 256 code points (0..255). Skip anything outside that - # range instead of indexing off the end of the cache / file. - code = ord(char) - if code > 255: - return None - offset = (code * self.height) + line - if self._cache: - if offset >= len(self._cache): - return None - return self._cache[offset] - - self._font.seek(offset) - try: - return struct.unpack("B", self._font.read(1))[0] - except RuntimeError: # maybe character isnt there? go to next - return None - - def export(self, filename): - """ - Export the font data in self._cache to a .py file that can be imported. - The format is a single bytes object named _FONT. There are 256 lines, one for each character. - The last line is `FONT = memoryview(_FONT)`. - - Args: - filename (str): The path to save the file to. - """ - if not self._cache: - raise RuntimeError("Font data not cached, cannot export") - mv = memoryview(self._cache) - with open(filename, "w") as f: - f.write("_FONT =\\\n") - for i in range(256): - f.write("b'") - for j in range(self.height): - f.write(f"\\x{mv[(i * self.height) + j]:02x}") - f.write("'\\\n") - f.write("\nFONT = memoryview(_FONT)\n") diff --git a/src/lib/graphics/_font_8x14.py b/src/lib/graphics/_font_8x14.py deleted file mode 100644 index a4810bc68..000000000 --- a/src/lib/graphics/_font_8x14.py +++ /dev/null @@ -1,259 +0,0 @@ -_FONT = ( - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x7e\x00\x00\x00" - b"\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\x7e\x00\x00\x00" - b"\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00" - b"\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00" - b"\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff" - b"\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00" - b"\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff" - b"\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\x78\x00\x00\x00" - b"\x00\x00\x3c\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00" - b"\x00\x00\x3f\x33\x3f\x30\x30\x30\x70\xf0\xe0\x00\x00\x00" - b"\x00\x00\x7f\x63\x7f\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00" - b"\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00" - b"\x00\x00\x80\xc0\xe0\xf8\xfe\xf8\xe0\xc0\x80\x00\x00\x00" - b"\x00\x00\x02\x06\x0e\x3e\xfe\x3e\x0e\x06\x02\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00" - b"\x00\x00\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00" - b"\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x00\x00\x00" - b"\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x00\x00\x00" - b"\x00\x00\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00" - b"\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00" - b"\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x3c\x3c\x18\x18\x00\x18\x18\x00\x00\x00" - b"\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00" - b"\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x86\xc6\x7c\x18\x18\x00" - b"\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x66\xc6\x00\x00\x00" - b"\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00" - b"\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00" - b"\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00" - b"\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xce\xde\xf6\xe6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x7e\x00\x00\x00" - b"\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc6\xfe\x00\x00\x00" - b"\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\xc6\x7c\x00\x00\x00" - b"\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x1e\x00\x00\x00" - b"\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\xc6\x7c\x00\x00\x00" - b"\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\xfe\xc6\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x0c\x78\x00\x00\x00" - b"\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00" - b"\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00" - b"\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x00\x18\x18\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00" - b"\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\xfc\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00" - b"\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00" - b"\x00\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00" - b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\xf0\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\x66\x3a\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00" - b"\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\x78\x00\x00\x00" - b"\x00\x00\xe6\x66\x6c\x6c\x78\x6c\x6c\x66\xe6\x00\x00\x00" - b"\x00\x00\xf0\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00" - b"\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\x00\x00\x00" - b"\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\xf0\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\xe6\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\x60\x38\x0c\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xfe\x7c\x6c\x00\x00\x00" - b"\x00\x00\xc6\xc6\x6c\x38\x38\x38\x6c\xc6\xc6\x00\x00\x00" - b"\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\xfe\xc6\x8c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00" - b"\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00" - b"\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00" - b"\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00" - b"\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00" - b"\x30\x30\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x7c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xf0\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00" - b"\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\xe6\x00\x00\x00" - b"\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x66\x66\x3c\x00" - b"\x00\x00\xe0\x60\x60\x66\x6c\x78\x6c\x66\xe6\x00\x00\x00" - b"\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xc6\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x7c\x60\x60\xf0\x00" - b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00" - b"\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\xf0\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\x70\x1c\xc6\x7c\x00\x00\x00" - b"\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x36\x1c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xfe\x6c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x6c\xc6\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00" - b"\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x66\xfe\x00\x00\x00" - b"\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x0e\x00\x00\x00" - b"\x00\x00\x18\x18\x18\x18\x00\x18\x18\x18\x18\x00\x00\x00" - b"\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x70\x00\x00\x00" - b"\x00\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\x00\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc2\x66\x3c\x0c\x06\x7c\x00" - b"\x00\x00\xcc\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\xcc\xcc\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\x00\x00\x3c\x66\x60\x66\x3c\x0c\x06\x3c\x00\x00" - b"\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x00\xcc\xcc\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" - b"\x00\x00\x66\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\xc6\xc6\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00" - b"\x38\x6c\x38\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00" - b"\x18\x30\x60\x00\xfe\x66\x60\x7c\x60\x66\xfe\x00\x00\x00" - b"\x00\x00\x00\x00\xcc\x76\x36\x7e\xd8\xd8\x6e\x00\x00\x00" - b"\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xce\x00\x00\x00" - b"\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\xc6\xc6\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00" - b"\x00\xc6\xc6\x38\x6c\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00" - b"\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x18\x18\x3c\x66\x60\x60\x66\x3c\x18\x18\x00\x00\x00" - b"\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xe6\xfc\x00\x00\x00" - b"\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x00\x00\x00" - b"\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xc6\x00\x00\x00" - b"\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\x18\xd8\x70\x00" - b"\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" - b"\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" - b"\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" - b"\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00" - b"\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\x00\x00\x00" - b"\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x30\x30\x00\x30\x30\x60\xc6\xc6\x7c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x00\x00\x00\x00" - b"\x00\xc0\xc0\xc6\xcc\xd8\x30\x60\xdc\x86\x0c\x18\x3e\x00" - b"\x00\xc0\xc0\xc6\xcc\xd8\x30\x66\xce\x9e\x3e\x06\x06\x00" - b"\x00\x00\x18\x18\x00\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00" - b"\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00" - b"\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44" - b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa" - b"\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77" - b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36" - b"\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36" - b"\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18" - b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" - b"\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff" - b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" - b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" - b"\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xdc\x76\x00\x00\x00" - b"\x00\x00\x00\x00\x7c\xc6\xfc\xc6\xc6\xfc\xc0\xc0\x40\x00" - b"\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00" - b"\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00" - b"\x00\x00\xfe\xc6\x60\x30\x18\x30\x60\xc6\xfe\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\x70\x00\x00\x00" - b"\x00\x00\x00\x00\x66\x66\x66\x66\x7c\x60\x60\xc0\x00\x00" - b"\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x00\x00\x00" - b"\x00\x00\x7e\x18\x3c\x66\x66\x66\x3c\x18\x7e\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x6c\x38\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\xee\x00\x00\x00" - b"\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x3c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\xdb\xdb\x7e\x00\x00\x00\x00\x00" - b"\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00" - b"\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x30\x1c\x00\x00\x00" - b"\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00" - b"\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00" - b"\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\xff\x00\x00\x00" - b"\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00" - b"\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00" - b"\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\x70\x00\x00\x00" - b"\x00\x00\x00\x18\x18\x00\x7e\x00\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00" - b"\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00" - b"\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x3c\x1c\x00\x00\x00" - b"\x00\xd8\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x70\xd8\x30\x60\xc8\xf8\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x7c\x7c\x7c\x7c\x7c\x7c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -) -FONT = memoryview(_FONT) diff --git a/src/lib/graphics/_font_8x16.py b/src/lib/graphics/_font_8x16.py deleted file mode 100644 index 6ad3c7522..000000000 --- a/src/lib/graphics/_font_8x16.py +++ /dev/null @@ -1,259 +0,0 @@ -_FONT = ( - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x81\x7e\x00\x00\x00\x00" - b"\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\xff\x7e\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00\x00" - b"\xff\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff\xff" - b"\x00\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00\x00" - b"\xff\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff\xff" - b"\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\xcc\x78\x00\x00\x00\x00" - b"\x00\x00\x3c\x66\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x3f\x33\x3f\x30\x30\x30\x30\x70\xf0\xe0\x00\x00\x00\x00" - b"\x00\x00\x7f\x63\x7f\x63\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00\x00" - b"\x00\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00\x00" - b"\x00\x80\xc0\xe0\xf0\xf8\xfe\xf8\xf0\xe0\xc0\x80\x00\x00\x00\x00" - b"\x00\x02\x06\x0e\x1e\x3e\xfe\x3e\x1e\x0e\x06\x02\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00\x00" - b"\x00\x00\x66\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00\x00" - b"\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x1b\x00\x00\x00\x00" - b"\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\xfe\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x18\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x18\x3c\x3c\x3c\x18\x18\x18\x00\x18\x18\x00\x00\x00\x00" - b"\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00\x00" - b"\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x06\x86\xc6\x7c\x18\x18\x00\x00" - b"\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x60\xc6\x86\x00\x00\x00\x00" - b"\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00\x00" - b"\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xd6\xd6\xc6\xc6\x6c\x38\x00\x00\x00\x00" - b"\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x18\x7e\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc0\xc6\xfe\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\x06\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x0c\x1e\x00\x00\x00\x00" - b"\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\x06\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\xfe\xc6\x06\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x06\x0c\x78\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00\x00" - b"\x00\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x18\x00\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00\x00" - b"\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\x66\xfc\x00\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00\x00" - b"\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00\x00" - b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x62\x66\xfe\x00\x00\x00\x00" - b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\x60\xf0\x00\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\xc6\x66\x3a\x00\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\xcc\x78\x00\x00\x00\x00" - b"\x00\x00\xe6\x66\x66\x6c\x78\x78\x6c\x66\x66\xe6\x00\x00\x00\x00" - b"\x00\x00\xf0\x60\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00\x00" - b"\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\x60\xf0\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00" - b"\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\x66\xe6\x00\x00\x00\x00" - b"\x00\x00\x7c\xc6\xc6\x60\x38\x0c\x06\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00\x00" - b"\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xd6\xfe\xee\x6c\x00\x00\x00\x00" - b"\x00\x00\xc6\xc6\x6c\x7c\x38\x38\x7c\x6c\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\xfe\xc6\x86\x0c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00\x00" - b"\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00\x00" - b"\x00\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00\x00" - b"\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00\x00" - b"\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00" - b"\x00\x30\x18\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x66\x7c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x1c\x36\x32\x30\x78\x30\x30\x30\x30\x78\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00" - b"\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\x66\xe6\x00\x00\x00\x00" - b"\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x06\x06\x66\x66\x3c\x00" - b"\x00\x00\xe0\x60\x60\x66\x6c\x78\x78\x6c\x66\xe6\x00\x00\x00\x00" - b"\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xd6\xc6\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x66\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x7c\x60\x60\xf0\x00" - b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00" - b"\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\x60\xf0\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\x60\x38\x0c\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x30\x36\x1c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xd6\xfe\x6c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x38\x6c\xc6\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00" - b"\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x60\xc6\xfe\x00\x00\x00\x00" - b"\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x18\x0e\x00\x00\x00\x00" - b"\x00\x00\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x18\x70\x00\x00\x00\x00" - b"\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xc6\xfe\x00\x00\x00\x00\x00" - b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc0\xc2\x66\x3c\x18\x70\x00\x00" - b"\x00\x00\xcc\x00\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\xcc\x00\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x18\x70\x00\x00" - b"\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\xc6\x00\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x66\x00\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\xc6\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x38\x6c\x38\x10\x38\x6c\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x0c\x18\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xec\x36\x36\x7e\xd8\xd8\x6e\x00\x00\x00\x00" - b"\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xcc\xce\x00\x00\x00\x00" - b"\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\xc6\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\xc6\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00" - b"\x00\xc6\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x18\x18\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x18\x18\x00\x00\x00\x00" - b"\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\x60\xe6\xfc\x00\x00\x00\x00" - b"\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x18\x00\x00\x00\x00" - b"\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xcc\xc6\x00\x00\x00\x00" - b"\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\xd8\x70\x00\x00\x00\x00" - b"\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" - b"\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" - b"\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x66\x00\x00\x00\x00" - b"\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x30\x30\x00\x30\x30\x60\xc0\xc6\xc6\x7c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x06\x00\x00\x00\x00\x00" - b"\x00\x60\xe0\x62\x66\x6c\x18\x30\x60\xdc\x86\x0c\x18\x3e\x00\x00" - b"\x00\x60\xe0\x62\x66\x6c\x18\x30\x66\xce\x9a\x3f\x06\x06\x00\x00" - b"\x00\x00\x18\x18\x00\x18\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00\x00" - b"\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44" - b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa" - b"\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77" - b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18\x18\x18" - b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" - b"\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" - b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" - b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" - b"\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xd8\xdc\x76\x00\x00\x00\x00" - b"\x00\x00\x78\xcc\xcc\xcc\xd8\xcc\xc6\xc6\xc6\xcc\x00\x00\x00\x00" - b"\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00" - b"\x00\x00\xfe\xc6\x60\x30\x18\x18\x30\x60\xc6\xfe\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\xd8\x70\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x66\x66\x66\x66\x66\x66\x7c\x60\x60\xc0\x00" - b"\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" - b"\x00\x00\x7e\x18\x3c\x66\x66\x66\x66\x3c\x18\x7e\x00\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00" - b"\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\x6c\xee\x00\x00\x00\x00" - b"\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x66\x3c\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x7e\xdb\xdb\xdb\x7e\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00\x00" - b"\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x60\x30\x1c\x00\x00\x00\x00" - b"\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" - b"\x00\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x7e\x00\x00\x00\x00" - b"\x00\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00\x00" - b"\x00\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00\x00" - b"\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\xd8\x70\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x00\x7e\x00\x18\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00\x00" - b"\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x6c\x3c\x1c\x00\x00\x00\x00" - b"\x00\x6c\x36\x36\x36\x36\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x3c\x66\x0c\x18\x32\x7e\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -) -FONT = memoryview(_FONT) diff --git a/src/lib/graphics/_font_8x8.py b/src/lib/graphics/_font_8x8.py deleted file mode 100644 index 8b3bef231..000000000 --- a/src/lib/graphics/_font_8x8.py +++ /dev/null @@ -1,259 +0,0 @@ -_FONT = ( - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x7e\x81\xa5\x81\xbd\x99\x81\x7e" - b"\x7e\xff\xdb\xff\xc3\xe7\xff\x7e" - b"\x6c\xfe\xfe\xfe\x7c\x38\x10\x00" - b"\x10\x38\x7c\xfe\x7c\x38\x10\x00" - b"\x38\x7c\x38\xfe\xfe\xd6\x10\x38" - b"\x10\x38\x7c\xfe\xfe\x7c\x10\x38" - b"\x00\x00\x18\x3c\x3c\x18\x00\x00" - b"\xff\xff\xe7\xc3\xc3\xe7\xff\xff" - b"\x00\x3c\x66\x42\x42\x66\x3c\x00" - b"\xff\xc3\x99\xbd\xbd\x99\xc3\xff" - b"\x0f\x07\x0f\x7d\xcc\xcc\xcc\x78" - b"\x3c\x66\x66\x66\x3c\x18\x7e\x18" - b"\x3f\x33\x3f\x30\x30\x70\xf0\xe0" - b"\x7f\x63\x7f\x63\x63\x67\xe6\xc0" - b"\x18\xdb\x3c\xe7\xe7\x3c\xdb\x18" - b"\x80\xe0\xf8\xfe\xf8\xe0\x80\x00" - b"\x02\x0e\x3e\xfe\x3e\x0e\x02\x00" - b"\x18\x3c\x7e\x18\x18\x7e\x3c\x18" - b"\x66\x66\x66\x66\x66\x00\x66\x00" - b"\x7f\xdb\xdb\x7b\x1b\x1b\x1b\x00" - b"\x3e\x61\x3c\x66\x66\x3c\x86\x7c" - b"\x00\x00\x00\x00\x7e\x7e\x7e\x00" - b"\x18\x3c\x7e\x18\x7e\x3c\x18\xff" - b"\x18\x3c\x7e\x18\x18\x18\x18\x00" - b"\x18\x18\x18\x18\x7e\x3c\x18\x00" - b"\x00\x18\x0c\xfe\x0c\x18\x00\x00" - b"\x00\x30\x60\xfe\x60\x30\x00\x00" - b"\x00\x00\xc0\xc0\xc0\xfe\x00\x00" - b"\x00\x24\x66\xff\x66\x24\x00\x00" - b"\x00\x18\x3c\x7e\xff\xff\x00\x00" - b"\x00\xff\xff\x7e\x3c\x18\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x18\x3c\x3c\x18\x18\x00\x18\x00" - b"\x66\x66\x24\x00\x00\x00\x00\x00" - b"\x6c\x6c\xfe\x6c\xfe\x6c\x6c\x00" - b"\x18\x3e\x60\x3c\x06\x7c\x18\x00" - b"\x00\xc6\xcc\x18\x30\x66\xc6\x00" - b"\x38\x6c\x38\x76\xdc\xcc\x76\x00" - b"\x18\x18\x30\x00\x00\x00\x00\x00" - b"\x0c\x18\x30\x30\x30\x18\x0c\x00" - b"\x30\x18\x0c\x0c\x0c\x18\x30\x00" - b"\x00\x66\x3c\xff\x3c\x66\x00\x00" - b"\x00\x18\x18\x7e\x18\x18\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x18\x30" - b"\x00\x00\x00\x7e\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x18\x18\x00" - b"\x06\x0c\x18\x30\x60\xc0\x80\x00" - b"\x38\x6c\xc6\xd6\xc6\x6c\x38\x00" - b"\x18\x38\x18\x18\x18\x18\x7e\x00" - b"\x7c\xc6\x06\x1c\x30\x66\xfe\x00" - b"\x7c\xc6\x06\x3c\x06\xc6\x7c\x00" - b"\x1c\x3c\x6c\xcc\xfe\x0c\x1e\x00" - b"\xfe\xc0\xc0\xfc\x06\xc6\x7c\x00" - b"\x38\x60\xc0\xfc\xc6\xc6\x7c\x00" - b"\xfe\xc6\x0c\x18\x30\x30\x30\x00" - b"\x7c\xc6\xc6\x7c\xc6\xc6\x7c\x00" - b"\x7c\xc6\xc6\x7e\x06\x0c\x78\x00" - b"\x00\x18\x18\x00\x00\x18\x18\x00" - b"\x00\x18\x18\x00\x00\x18\x18\x30" - b"\x06\x0c\x18\x30\x18\x0c\x06\x00" - b"\x00\x00\x7e\x00\x00\x7e\x00\x00" - b"\x60\x30\x18\x0c\x18\x30\x60\x00" - b"\x7c\xc6\x0c\x18\x18\x00\x18\x00" - b"\x7c\xc6\xde\xde\xde\xc0\x78\x00" - b"\x38\x6c\xc6\xfe\xc6\xc6\xc6\x00" - b"\xfc\x66\x66\x7c\x66\x66\xfc\x00" - b"\x3c\x66\xc0\xc0\xc0\x66\x3c\x00" - b"\xf8\x6c\x66\x66\x66\x6c\xf8\x00" - b"\xfe\x62\x68\x78\x68\x62\xfe\x00" - b"\xfe\x62\x68\x78\x68\x60\xf0\x00" - b"\x3c\x66\xc0\xc0\xce\x66\x3a\x00" - b"\xc6\xc6\xc6\xfe\xc6\xc6\xc6\x00" - b"\x3c\x18\x18\x18\x18\x18\x3c\x00" - b"\x1e\x0c\x0c\x0c\xcc\xcc\x78\x00" - b"\xe6\x66\x6c\x78\x6c\x66\xe6\x00" - b"\xf0\x60\x60\x60\x62\x66\xfe\x00" - b"\xc6\xee\xfe\xfe\xd6\xc6\xc6\x00" - b"\xc6\xe6\xf6\xde\xce\xc6\xc6\x00" - b"\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00" - b"\xfc\x66\x66\x7c\x60\x60\xf0\x00" - b"\x7c\xc6\xc6\xc6\xc6\xce\x7c\x0e" - b"\xfc\x66\x66\x7c\x6c\x66\xe6\x00" - b"\x3c\x66\x30\x18\x0c\x66\x3c\x00" - b"\x7e\x7e\x5a\x18\x18\x18\x3c\x00" - b"\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00" - b"\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00" - b"\xc6\xc6\xc6\xd6\xd6\xfe\x6c\x00" - b"\xc6\xc6\x6c\x38\x6c\xc6\xc6\x00" - b"\x66\x66\x66\x3c\x18\x18\x3c\x00" - b"\xfe\xc6\x8c\x18\x32\x66\xfe\x00" - b"\x3c\x30\x30\x30\x30\x30\x3c\x00" - b"\xc0\x60\x30\x18\x0c\x06\x02\x00" - b"\x3c\x0c\x0c\x0c\x0c\x0c\x3c\x00" - b"\x10\x38\x6c\xc6\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xff" - b"\x30\x18\x0c\x00\x00\x00\x00\x00" - b"\x00\x00\x78\x0c\x7c\xcc\x76\x00" - b"\xe0\x60\x7c\x66\x66\x66\xdc\x00" - b"\x00\x00\x7c\xc6\xc0\xc6\x7c\x00" - b"\x1c\x0c\x7c\xcc\xcc\xcc\x76\x00" - b"\x00\x00\x7c\xc6\xfe\xc0\x7c\x00" - b"\x3c\x66\x60\xf8\x60\x60\xf0\x00" - b"\x00\x00\x76\xcc\xcc\x7c\x0c\xf8" - b"\xe0\x60\x6c\x76\x66\x66\xe6\x00" - b"\x18\x00\x38\x18\x18\x18\x3c\x00" - b"\x06\x00\x06\x06\x06\x66\x66\x3c" - b"\xe0\x60\x66\x6c\x78\x6c\xe6\x00" - b"\x38\x18\x18\x18\x18\x18\x3c\x00" - b"\x00\x00\xec\xfe\xd6\xd6\xd6\x00" - b"\x00\x00\xdc\x66\x66\x66\x66\x00" - b"\x00\x00\x7c\xc6\xc6\xc6\x7c\x00" - b"\x00\x00\xdc\x66\x66\x7c\x60\xf0" - b"\x00\x00\x76\xcc\xcc\x7c\x0c\x1e" - b"\x00\x00\xdc\x76\x60\x60\xf0\x00" - b"\x00\x00\x7e\xc0\x7c\x06\xfc\x00" - b"\x30\x30\xfc\x30\x30\x36\x1c\x00" - b"\x00\x00\xcc\xcc\xcc\xcc\x76\x00" - b"\x00\x00\xc6\xc6\xc6\x6c\x38\x00" - b"\x00\x00\xc6\xd6\xd6\xfe\x6c\x00" - b"\x00\x00\xc6\x6c\x38\x6c\xc6\x00" - b"\x00\x00\xc6\xc6\xc6\x7e\x06\xfc" - b"\x00\x00\x7e\x4c\x18\x32\x7e\x00" - b"\x0e\x18\x18\x70\x18\x18\x0e\x00" - b"\x18\x18\x18\x18\x18\x18\x18\x00" - b"\x70\x18\x18\x0e\x18\x18\x70\x00" - b"\x76\xdc\x00\x00\x00\x00\x00\x00" - b"\x00\x10\x38\x6c\xc6\xc6\xfe\x00" - b"\x7c\xc6\xc0\xc0\xc6\x7c\x0c\x78" - b"\xcc\x00\xcc\xcc\xcc\xcc\x76\x00" - b"\x0c\x18\x7c\xc6\xfe\xc0\x7c\x00" - b"\x7c\x82\x78\x0c\x7c\xcc\x76\x00" - b"\xc6\x00\x78\x0c\x7c\xcc\x76\x00" - b"\x30\x18\x78\x0c\x7c\xcc\x76\x00" - b"\x30\x30\x78\x0c\x7c\xcc\x76\x00" - b"\x00\x00\x7e\xc0\xc0\x7e\x0c\x38" - b"\x7c\x82\x7c\xc6\xfe\xc0\x7c\x00" - b"\xc6\x00\x7c\xc6\xfe\xc0\x7c\x00" - b"\x30\x18\x7c\xc6\xfe\xc0\x7c\x00" - b"\x66\x00\x38\x18\x18\x18\x3c\x00" - b"\x7c\x82\x38\x18\x18\x18\x3c\x00" - b"\x30\x18\x00\x38\x18\x18\x3c\x00" - b"\xc6\x38\x6c\xc6\xfe\xc6\xc6\x00" - b"\x38\x6c\x7c\xc6\xfe\xc6\xc6\x00" - b"\x18\x30\xfe\xc0\xf8\xc0\xfe\x00" - b"\x00\x00\x7e\x18\x7e\xd8\x7e\x00" - b"\x3e\x6c\xcc\xfe\xcc\xcc\xce\x00" - b"\x7c\x82\x7c\xc6\xc6\xc6\x7c\x00" - b"\xc6\x00\x7c\xc6\xc6\xc6\x7c\x00" - b"\x30\x18\x7c\xc6\xc6\xc6\x7c\x00" - b"\x78\x84\x00\xcc\xcc\xcc\x76\x00" - b"\x60\x30\xcc\xcc\xcc\xcc\x76\x00" - b"\xc6\x00\xc6\xc6\xc6\x7e\x06\xfc" - b"\xc6\x38\x6c\xc6\xc6\x6c\x38\x00" - b"\xc6\x00\xc6\xc6\xc6\xc6\x7c\x00" - b"\x18\x18\x7e\xc0\xc0\x7e\x18\x18" - b"\x38\x6c\x64\xf0\x60\x66\xfc\x00" - b"\x66\x66\x3c\x7e\x18\x7e\x18\x18" - b"\xf8\xcc\xcc\xfa\xc6\xcf\xc6\xc7" - b"\x0e\x1b\x18\x3c\x18\xd8\x70\x00" - b"\x18\x30\x78\x0c\x7c\xcc\x76\x00" - b"\x0c\x18\x00\x38\x18\x18\x3c\x00" - b"\x0c\x18\x7c\xc6\xc6\xc6\x7c\x00" - b"\x18\x30\xcc\xcc\xcc\xcc\x76\x00" - b"\x76\xdc\x00\xdc\x66\x66\x66\x00" - b"\x76\xdc\x00\xe6\xf6\xde\xce\x00" - b"\x3c\x6c\x6c\x3e\x00\x7e\x00\x00" - b"\x38\x6c\x6c\x38\x00\x7c\x00\x00" - b"\x18\x00\x18\x18\x30\x63\x3e\x00" - b"\x00\x00\x00\xfe\xc0\xc0\x00\x00" - b"\x00\x00\x00\xfe\x06\x06\x00\x00" - b"\x63\xe6\x6c\x7e\x33\x66\xcc\x0f" - b"\x63\xe6\x6c\x7a\x36\x6a\xdf\x06" - b"\x18\x00\x18\x18\x3c\x3c\x18\x00" - b"\x00\x33\x66\xcc\x66\x33\x00\x00" - b"\x00\xcc\x66\x33\x66\xcc\x00\x00" - b"\x22\x88\x22\x88\x22\x88\x22\x88" - b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa" - b"\x77\xdd\x77\xdd\x77\xdd\x77\xdd" - b"\x18\x18\x18\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\xf8\x18\x18\x18" - b"\x18\x18\xf8\x18\xf8\x18\x18\x18" - b"\x36\x36\x36\x36\xf6\x36\x36\x36" - b"\x00\x00\x00\x00\xfe\x36\x36\x36" - b"\x00\x00\xf8\x18\xf8\x18\x18\x18" - b"\x36\x36\xf6\x06\xf6\x36\x36\x36" - b"\x36\x36\x36\x36\x36\x36\x36\x36" - b"\x00\x00\xfe\x06\xf6\x36\x36\x36" - b"\x36\x36\xf6\x06\xfe\x00\x00\x00" - b"\x36\x36\x36\x36\xfe\x00\x00\x00" - b"\x18\x18\xf8\x18\xf8\x00\x00\x00" - b"\x00\x00\x00\x00\xf8\x18\x18\x18" - b"\x18\x18\x18\x18\x1f\x00\x00\x00" - b"\x18\x18\x18\x18\xff\x00\x00\x00" - b"\x00\x00\x00\x00\xff\x18\x18\x18" - b"\x18\x18\x18\x18\x1f\x18\x18\x18" - b"\x00\x00\x00\x00\xff\x00\x00\x00" - b"\x18\x18\x18\x18\xff\x18\x18\x18" - b"\x18\x18\x1f\x18\x1f\x18\x18\x18" - b"\x36\x36\x36\x36\x37\x36\x36\x36" - b"\x36\x36\x37\x30\x3f\x00\x00\x00" - b"\x00\x00\x3f\x30\x37\x36\x36\x36" - b"\x36\x36\xf7\x00\xff\x00\x00\x00" - b"\x00\x00\xff\x00\xf7\x36\x36\x36" - b"\x36\x36\x37\x30\x37\x36\x36\x36" - b"\x00\x00\xff\x00\xff\x00\x00\x00" - b"\x36\x36\xf7\x00\xf7\x36\x36\x36" - b"\x18\x18\xff\x00\xff\x00\x00\x00" - b"\x36\x36\x36\x36\xff\x00\x00\x00" - b"\x00\x00\xff\x00\xff\x18\x18\x18" - b"\x00\x00\x00\x00\xff\x36\x36\x36" - b"\x36\x36\x36\x36\x3f\x00\x00\x00" - b"\x18\x18\x1f\x18\x1f\x00\x00\x00" - b"\x00\x00\x1f\x18\x1f\x18\x18\x18" - b"\x00\x00\x00\x00\x3f\x36\x36\x36" - b"\x36\x36\x36\x36\xff\x36\x36\x36" - b"\x18\x18\xff\x18\xff\x18\x18\x18" - b"\x18\x18\x18\x18\xf8\x00\x00\x00" - b"\x00\x00\x00\x00\x1f\x18\x18\x18" - b"\xff\xff\xff\xff\xff\xff\xff\xff" - b"\x00\x00\x00\x00\xff\xff\xff\xff" - b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" - b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" - b"\xff\xff\xff\xff\x00\x00\x00\x00" - b"\x00\x00\x76\xdc\xc8\xdc\x76\x00" - b"\x78\xcc\xcc\xd8\xcc\xc6\xcc\x00" - b"\xfe\xc6\xc0\xc0\xc0\xc0\xc0\x00" - b"\x00\x00\xfe\x6c\x6c\x6c\x6c\x00" - b"\xfe\xc6\x60\x30\x60\xc6\xfe\x00" - b"\x00\x00\x7e\xd8\xd8\xd8\x70\x00" - b"\x00\x00\x66\x66\x66\x66\x7c\xc0" - b"\x00\x76\xdc\x18\x18\x18\x18\x00" - b"\x7e\x18\x3c\x66\x66\x3c\x18\x7e" - b"\x38\x6c\xc6\xfe\xc6\x6c\x38\x00" - b"\x38\x6c\xc6\xc6\x6c\x6c\xee\x00" - b"\x0e\x18\x0c\x3e\x66\x66\x3c\x00" - b"\x00\x00\x7e\xdb\xdb\x7e\x00\x00" - b"\x06\x0c\x7e\xdb\xdb\x7e\x60\xc0" - b"\x1e\x30\x60\x7e\x60\x30\x1e\x00" - b"\x00\x7c\xc6\xc6\xc6\xc6\xc6\x00" - b"\x00\xfe\x00\xfe\x00\xfe\x00\x00" - b"\x18\x18\x7e\x18\x18\x00\x7e\x00" - b"\x30\x18\x0c\x18\x30\x00\x7e\x00" - b"\x0c\x18\x30\x18\x0c\x00\x7e\x00" - b"\x0e\x1b\x1b\x18\x18\x18\x18\x18" - b"\x18\x18\x18\x18\x18\xd8\xd8\x70" - b"\x00\x18\x00\x7e\x00\x18\x00\x00" - b"\x00\x76\xdc\x00\x76\xdc\x00\x00" - b"\x38\x6c\x6c\x38\x00\x00\x00\x00" - b"\x00\x00\x00\x18\x18\x00\x00\x00" - b"\x00\x00\x00\x18\x00\x00\x00\x00" - b"\x0f\x0c\x0c\x0c\xec\x6c\x3c\x1c" - b"\x6c\x36\x36\x36\x36\x00\x00\x00" - b"\x78\x0c\x18\x30\x7c\x00\x00\x00" - b"\x00\x00\x3c\x3c\x3c\x3c\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00" -) -FONT = memoryview(_FONT) diff --git a/src/lib/graphics/_framebuf_plus.py b/src/lib/graphics/_framebuf_plus.py deleted file mode 100644 index 7db66273d..000000000 --- a/src/lib/graphics/_framebuf_plus.py +++ /dev/null @@ -1,644 +0,0 @@ -from . import _files, _font, _shapes -from ._area import Area -from ._blit_hooks import clip_blit_bounds - -try: - # Local submodule: present in TestPyPI wheels and after scripts/install_sync_framebuf.py runs. - from .framebuf import ( - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_HMSB, - MONO_VLSB, - RGB565, - ) - from .framebuf import FrameBuffer as _FrameBuffer -except ImportError: - # graphics/framebuf.py is a generated packaging artifact (gitignored) not present - # in all deploy paths (PyScript, mip-install from GitHub, bare CP/MP without wheel). - # Fall back to the bare framebuf module: native C on MicroPython hardware, - # or add_ons/framebuf.py when add_ons/ is on sys.path (PyScript, CircuitPython, etc.). - from framebuf import ( # type: ignore[no-redef] - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_HMSB, - MONO_VLSB, - RGB565, - ) - from framebuf import FrameBuffer as _FrameBuffer # type: ignore[no-redef] - -# pydisplay extension — not in MicroPython framebuf -RGB888 = 7 - - -class _RGB888Format: - depth = 24 - - @staticmethod - def set_pixel(framebuf, x, y, color): - index = (y * framebuf._stride + x) * 3 - framebuf._buffer[index : index + 3] = bytes( - ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) - ) - - @staticmethod - def get_pixel(framebuf, x, y): - index = (y * framebuf._stride + x) * 3 - r, g, b = framebuf._buffer[index : index + 3] - return (r << 16) | (g << 8) | b - - @staticmethod - def fill(framebuf, color): - rgb = bytes(((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)) - for i in range(0, len(framebuf._buffer), 3): - framebuf._buffer[i : i + 3] = rgb - - @staticmethod - def fill_rect(framebuf, x, y, width, height, color): - rgb = bytes(((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)) - stride = framebuf._stride - for _y in range(y, y + height): - row = _y * stride - for _x in range(x, x + width): - index = (row + _x) * 3 - framebuf._buffer[index : index + 3] = rgb - - -class FrameBuffer(_FrameBuffer): - """ - An extension of MicroPython's framebuf.FrameBuffer that adds some useful methods for drawing shapes and text. - Each method returns a bounding box (x, y, w, h) of the drawn shape to indicate - the area of the display that was modified. This can be used to update only the - modified area of the display. Exposes attributes not exposed in the base class, such - as color_depth, width, height, buffer, and format. Also adds a save method to save - the framebuffer to a file, and a from_file method to load a framebuffer from a file. - - Inherits from the bundled pure-Python ``.framebuf.FrameBuffer`` (never the - compiled native ``framebuf`` module, to guarantee identical behavior across - runtimes). Methods should return an Area object, but the MicroPython - framebuf module returns None, so the methods inherited from - framebuf.FrameBuffer are overridden to return an Area object. - - Args: - buffer (bytearray): Framebuffer buffer - width (int): Width in pixels - height (int): Height in pixels - format (int): Framebuffer format - - Attributes: - buffer (bytearray): Framebuffer buffer - width (int): Width in pixels - height (int): Height in pixels - format (int): Framebuffer format - color_depth (int): Color depth - """ - - def __init__(self, buffer, width, height, format, *args, **kwargs): - self._rgb888 = format == RGB888 - # RGB888 is a pydisplay extension; base framebuf never implements it. - # Initialize the C extmod base as RGB565 so MicroPython does not fall back - # to make_new with an invalid format. Pixel ops use _RGB888Format below. - if self._rgb888: - super().__init__(buffer, width, height, RGB565) - else: - super().__init__(buffer, width, height, format, *args, **kwargs) - self._width = width - self._height = height - self._fb_format = format - self._buffer = buffer - if self._rgb888: - stride = args[0] if args else kwargs.get("stride") - self._stride = stride if stride is not None else width - self._color_depth = 24 - elif format in (MONO_VLSB, MONO_HLSB, MONO_HMSB): - self._color_depth = 1 - elif format == RGB565: - self._color_depth = 16 - elif format == GS2_HMSB: - self._color_depth = 2 - elif format == GS4_HMSB: - self._color_depth = 4 - elif format == GS8: - self._color_depth = 8 - else: - raise ValueError("invalid format") - - @property - def color_depth(self): - return self._color_depth - - @property - def width(self): - return self._width - - @property - def height(self): - return self._height - - @property - def buffer(self): - return self._buffer - - @property - def format(self): - return self._fb_format - - def fill_rect(self, x, y, w, h, c): - """ - Fill the given rectangle with the given color. - - Args: - x (int): x coordinate - y (int): y coordinate - w (int): Width in pixels - h (int): Height in pixels - c (int): color - - Returns: - (Area): Bounding box of the filled rectangle - """ - if self._rgb888: - _RGB888Format.fill_rect(self, x, y, w, h, c) - return Area(x, y, w, h) - super().fill_rect(x, y, w, h, c) - return Area(x, y, w, h) - - def pixel(self, x, y, c=None): - """ - Draw a single pixel at the given location and color. - - Args: - x (int): x coordinate - y (int): y coordinate - c (int): color (default: None) - - Returns: - (Area): Bounding box of the pixel - """ - if self._rgb888: - if c is None: - return _RGB888Format.get_pixel(self, x, y) - _RGB888Format.set_pixel(self, x, y, c) - return Area(x, y, 1, 1) - if c is None: - return super().pixel(x, y) - super().pixel(x, y, c) - return Area(x, y, 1, 1) - - def fill(self, c): - """ - Fill the buffer with the given color. - - Args: - c (int): color - - Returns: - (Area): Bounding box of the filled buffer - """ - if self._rgb888: - _RGB888Format.fill(self, c) - return Area(0, 0, self.width, self.height) - super().fill(c) - return Area(0, 0, self.width, self.height) - - def ellipse(self, x, y, rx, ry, c, f=False, m=0b1111): - """ - Draw an ellipse at the given location, radii and color. - - Args: - x (int): Center x coordinate - y (int): Center y coordinate - rx (int): X radius - ry (int): Y radius - c (int): color - f (bool): Fill the ellipse (default: False) - m (int): Bitmask to determine which quadrants to draw (default: 0b1111) - - Returns: - (Area): Bounding box of the ellipse - """ - super().ellipse(x, y, rx, ry, c, f, m) - return Area(x - rx, y - ry, 2 * rx, 2 * ry) - - def hline(self, x, y, w, c): - """ - Draw a horizontal line at the given location, width and color. - - Args: - x (int): x coordinate - y (int): y coordinate - w (int): Width in pixels - c (int): color - - Returns: - (Area): Bounding box of the horizontal line - """ - super().hline(x, y, w, c) - return Area(x, y, w, 1) - - def line(self, x1, y1, x2, y2, c): - """ - Draw a line between the given start and end points and color. - - Args: - x1 (int): Start x coordinate - y1 (int): Start y coordinate - x2 (int): End x coordinate - y2 (int): End y coordinate - c (int): color - - Returns: - (Area): Bounding box of the line - """ - super().line(x1, y1, x2, y2, c) - return Area(min(x1, x2), min(y1, y2), abs(x2 - x1) + 1, abs(y2 - y1) + 1) - - def poly(self, x, y, coords, c, f=False): - """ - Draw a polygon at the given location, coordinates and color. - - Args: - x (int): x coordinate - y (int): y coordinate - coords (array): Array of x, y coordinate tuples - c (int): color - f (bool): Fill the polygon (default: False) - - Returns: - (Area): Bounding box of the polygon - """ - super().poly(x, y, coords, c, f) - # Calculate the bounding box of the polygon - # Convert the coords to a list of x, y tuples if it is not already - if isinstance(coords, list): - vertices = coords - elif isinstance(coords, tuple): - vertices = list(coords) - else: - # Check that the coords array has an even number of elements - if len(coords) % 2 != 0: - raise ValueError("coords must have an even number of elements") - vertices = [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] - # Find the min and max x and y values - min_x = min([v[0] for v in vertices]) - min_y = min([v[1] for v in vertices]) - max_x = max([v[0] for v in vertices]) - max_y = max([v[1] for v in vertices]) - return Area(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1) - - def rect(self, x, y, w, h, c, f=False): - """ - Draw a rectangle at the given location, size and color. - - Args: - x (int): Top left corner x coordinate - y (int): Top left corner y coordinate - w (int): Width in pixels - h (int): Height in pixels - c (int): color - f (bool): Fill the rectangle (default: False) - - Returns: - (Area): Bounding box of the rectangle - """ - super().rect(x, y, w, h, c, f) - return Area(x, y, w, h) - - def vline(self, x, y, h, c): - """ - Draw a vertical line at the given location, height and color. - - Args: - x (int): x coordinate - y (int): y coordinate - h (int): Height in pixels - c (int): color - - Returns: - (Area): Bounding box of the vertical line - """ - super().vline(x, y, h, c) - return Area(x, y, 1, h) - - def text(self, s, x, y, c=1, scale=1, inverted=False, font_data=None, height=8): - """ - Draw text at the given location, using the given font and color. - - Args: - s (str): Text to draw - x (int): x coordinate - y (int): y coordinate - c (int): color - scale (int): Scale factor (default: 1) - inverted (bool): Invert the text (default: False) - font_data (str): Path to the font file (default: None) - height (int): Height of the font (default: 8) - - Returns: - (Area): Bounding box of the text - """ - return _font.text( - self, s, x, y, c, scale=scale, inverted=inverted, font_data=font_data, height=height - ) - - def blit(self, source, x, y, key=-1, palette=None): - """ - Blit the given buffer at the given location. - - Args: - source (FrameBuffer): FrameBuffer to blit - x (int): x coordinate - y (int): y coordinate - key (int): Color key (default: -1) - palette (FrameBuffer): Palette (default: None) - - Returns: - (Area): Bounding box of the blitted buffer - """ - super().blit(source, x, y, key, palette) - clipped = clip_blit_bounds(self, source, x, y) - if clipped is None: - return None - x0, y0, w, h, _, _ = clipped - return Area(x0, y0, w, h) - - ########### Additional methods - - def arc(self, x, y, r, a0, a1, c): - """ - Arc drawing function. Will draw a single pixel wide arc with a radius r - centered at x, y from a0 to a1. - - Args: - x (int): X-coordinate of the arc's center. - y (int): Y-coordinate of the arc's center. - r (int): Radius of the arc. - a0 (float): Starting angle in degrees. - a1 (float): Ending angle in degrees. - c (int): color. - - Returns: - (Area): The bounding box of the arc. - """ - return _shapes.arc(self, x, y, r, a0, a1, c) - - def blit_rect(self, buf, x, y, w, h): - """ - Blit a rectangular area from a buffer to the canvas. Uses the canvas's blit_rect method if available, - otherwise writes directly to the buffer. - - Args: - buf (memoryview): Buffer to blit. Must already be byte-swapped if necessary. - x (int): X-coordinate to blit to. - y (int): Y-coordinate to blit to. - w (int): Width of the area to blit. - h (int): Height of the area to blit. - - Returns: - (Area): The bounding box of the blitted area. - """ - return _shapes.blit_rect(self, buf, x, y, w, h) - - def blit_transparent(self, buf, x, y, w, h, key): - """ - Blit a buffer with transparency. - - Args: - buf (memoryview): Buffer to blit. - x (int): X-coordinate to blit to. - y (int): Y-coordinate to blit to. - w (int): Width of the area to blit. - h (int): Height of the area to blit. - key (int): Key value for transparency. - - Returns: - (Area): The bounding box of the blitted area. - """ - return _shapes.blit_transparent(self, buf, x, y, w, h, key) - - def circle(self, x0, y0, r, c, f=False): - """ - Circle drawing function. Will draw a single pixel wide circle - centered at x0, y0 and the specified r. - - Args: - x0 (int): Center x coordinate - y0 (int): Center y coordinate - r (int): Radius - c (int): Color - f (bool): Fill the circle (default: False) - - Returns: - (Area): The bounding box of the circle. - """ - return _shapes.circle(self, x0, y0, r, c, f) - - def gradient_rect(self, x, y, w, h, c1, c2=None, vertical=True): - """ - Fill a rectangle with a gradient. - - Args: - x (int): X-coordinate of the top-left corner of the rectangle. - y (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - c1 (int): 565 encoded color for the top or left edge. - c2 (int): 565 encoded color for the bottom or right edge. If None or the same as c1, - fill_rect will be called instead. - vertical (bool): If True, the gradient will be vertical. If False, the gradient will be horizontal. - - Returns: - (Area): The bounding box of the filled area. - """ - return _shapes.gradient_rect(self, x, y, w, h, c1, c2, vertical) - - def polygon(self, points, x, y, color, angle=0, center_x=0, center_y=0): - """ - Draw a polygon on the canvas. - - Args: - points (list): List of points to draw. - x (int): X-coordinate of the polygon's position. - y (int): Y-coordinate of the polygon's position. - color (int): color. - angle (float): Rotation angle in radians (default: 0). - center_x (int): X-coordinate of the rotation center (default: 0). - center_y (int): Y-coordinate of the rotation center (default: 0). - - Raises: - ValueError: If the polygon has less than 3 points. - - Returns: - (Area): The bounding box of the polygon. - """ - return _shapes.polygon(self, points, x, y, color, angle, center_x, center_y) - - def round_rect(self, x0, y0, w, h, r, c, f=False): - """ - Rounded rectangle drawing function. Will draw a single pixel wide rounded rectangle starting at - x0, y0 and extending w, h pixels with the specified radius. - - Args: - x0 (int): X-coordinate of the top-left corner of the rectangle. - y0 (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - r (int): Radius of the corners. - c (int): color. - f (bool): Fill the rectangle (default: False). - - Returns: - (Area): The bounding box of the rectangle. - """ - return _shapes.round_rect(self, x0, y0, w, h, r, c, f) - - def triangle(self, x0, y0, x1, y1, x2, y2, c, f=False): - """ - Triangle drawing function. Draws a single pixel wide triangle with vertices at - (x0, y0), (x1, y1), and (x2, y2). - - Args: - x0 (int): X-coordinate of the first vertex. - y0 (int): Y-coordinate of the first vertex. - x1 (int): X-coordinate of the second vertex. - y1 (int): Y-coordinate of the second vertex. - x2 (int): X-coordinate of the third vertex. - y2 (int): Y-coordinate of the third vertex. - c (int): color. - f (bool): Fill the triangle (default: False). - - Returns: - (Area): The bounding box of the triangle. - """ - return _shapes.triangle(self, x0, y0, x1, y1, x2, y2, c, f) - - def text8(self, s, x, y, c=1, scale=1, inverted=False, font_data=None): - """ - Place text on the canvas with an 8 pixel high font. - Breaks on \n to next line. Does not break on line going off canvas. - - Args: - s (str): The text to draw. - x (int): The x position to start drawing the text. - y (int): The y position to start drawing the text. - c (int): The color to draw the text in. Default is 1. - scale (int): The scale factor to draw the text at. Default is 1. - inverted (bool): If True, draw the text inverted. Default is False. - font_data (str): The path to the font file to use. Default is None. - - Returns: - Area: The area that was drawn to. - """ - return _font.text8(self, s, x, y, c, scale, inverted, font_data) - - def text14(self, s, x, y, c=1, scale=1, inverted=False, font_data=None): - """ - Place text on the canvas with a 14 pixel high font. - Breaks on \n to next line. Does not break on line going off canvas. - - Args: - s (str): The text to draw. - x (int): The x position to start drawing the text. - y (int): The y position to start drawing the text. - c (int): The color to draw the text in. Default is 1. - scale (int): The scale factor to draw the text at. Default is 1. - inverted (bool): If True, draw the text inverted. Default is False. - font_data (str): The path to the font file to use. Default is None. - - Returns: - Area: The area that was drawn to. - """ - return _font.text14(self, s, x, y, c, scale, inverted, font_data) - - def text16(self, s, x, y, c=1, scale=1, inverted=False, font_data=None): - """ - Place text on the canvas with a 16 pixel high font. - Breaks on \n to next line. Does not break on line going off canvas. - - Args: - s (str): The text to draw. - x (int): The x position to start drawing the text. - y (int): The y position to start drawing the text. - c (int): The color to draw the text in. Default is 1. - scale (int): The scale factor to draw the text at. Default is 1. - inverted (bool): If True, draw the text inverted. Default is False. - font_data (str): The path to the font file to use. Default is None. - - Returns: - Area: The area that was drawn to. - """ - return _font.text16(self, s, x, y, c, scale, inverted, font_data) - - def scroll(self, xstep, ystep): - """Scroll buffer contents. Returns the full buffer bounds.""" - super().scroll(xstep, ystep) - return Area(0, 0, self.width, self.height) - - def save(self, filename=None): - """ - Save the framebuffer to a file. The file extension must match the format, otherwise - the extension will be appended to the filename. - - Saves 1-bit formats as PBM, 2-bit formats as PGM with max value 3, 4-bit formats as PGM with max value 15, - 8-bit formats as PGM with max value 255, and 16-bit formats as BMP. - - Args: - filename (str): Filename to save to - """ - return _files.save_image(self, filename) - - def export(self, filename): - """Export this framebuffer as an importable ``.py`` bitmap module. - - See :func:`graphics._files.export_framebuffer`. Ships ``BITMAP`` as a - ``bytearray`` for zero-copy :meth:`from_bitmap` on MicroPython. - """ - return _files.export_framebuffer(self, filename) - - @staticmethod - def from_file(filename): - """ - Load a framebuffer from a file. - - Args: - filename (str): Filename to load from - """ - return _files.load_image(filename) - - @staticmethod - def from_bitmap(buffer, width, height, format): - """Build a ``FrameBuffer`` from a bitmap buffer (export-module style). - - If ``buffer`` is already a writable ``bytearray`` (or a writable - ``memoryview``), it is used without copying. Legacy ``bytes`` / - ``memoryview(bytes)`` modules get one ``bytearray`` copy so MicroPython - can own a writable pixel buffer. - """ - if isinstance(buffer, bytearray): - buf = buffer - elif isinstance(buffer, memoryview): - writable = False - try: - underlying = buffer.obj # CPython - writable = isinstance(underlying, bytearray) - except AttributeError: - # MicroPython: probe whether the view accepts assignment. - try: - if len(buffer): - tmp = buffer[0] - buffer[0] = tmp - writable = True - except (TypeError, AttributeError): - writable = False - buf = buffer if writable else bytearray(buffer) - else: - buf = bytearray(buffer) - return FrameBuffer(buf, width, height, format) - - @staticmethod - def from_module(mod): - """Load from a module with ``WIDTH``, ``HEIGHT``, ``FORMAT``, ``BITMAP``.""" - return FrameBuffer.from_bitmap(mod.BITMAP, mod.WIDTH, mod.HEIGHT, mod.FORMAT) diff --git a/src/lib/graphics/_shapes.py b/src/lib/graphics/_shapes.py deleted file mode 100644 index a068803bf..000000000 --- a/src/lib/graphics/_shapes.py +++ /dev/null @@ -1,888 +0,0 @@ -# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries, 2024 Brad Barnett -# -# SPDX-License-Identifier: MIT -""" -`graphics._shapes` -==================================================== -Graphics primitives for drawing on a canvas. - -Heavily modified from gfx.py at: -https://github.com/adafruit/Adafruit_CircuitPython_GFX -* Author(s): Kattni Rembor, Tony DiCola, Jonah Yolles-Murphy, based on code by Phil Burgess - -Implementation Notes --------------------- -.pixel(), .fill_rect(), .fill() and .blit_rect() will be called from the canvas object if the canvas -object has these methods. - -.pixel() and .blit_rect() assume 16-bit color depth. - -""" - -import math - -from ._area import Area -from ._blit_hooks import ( - blit_rect_dispatch, - canvas_accepts_blit_transparent, - try_fast_framebuffer_blit, -) - - -def arc(canvas, x, y, r, a0, a1, c): - """ - Arc drawing function. Will draw a single pixel wide arc with a radius r - centered at x, y from a0 to a1. - - Args: - x (int): X-coordinate of the arc's center. - y (int): Y-coordinate of the arc's center. - r (int): Radius of the arc. - a0 (float): Starting angle in degrees. - a1 (float): Ending angle in degrees. - c (int): color. - - Returns: - (Area): The bounding box of the arc. - """ - resolution = 60 - a0 = math.radians(a0) - a1 = math.radians(a1) - x0 = x + int(r * math.cos(a0)) - y0 = y + int(r * math.sin(a0)) - if a1 > a0: - arc_range = range(int(a0 * resolution), int(a1 * resolution)) - else: - arc_range = range(int(a0 * resolution), int(a1 * resolution), -1) - - x_min = x_max = x0 - y_min = y_max = y0 - for a in arc_range: - ar = a / resolution - x1 = x + int(r * math.cos(ar)) - y1 = y + int(r * math.sin(ar)) - line(canvas, x0, y0, x1, y1, c) - x_min = min(x0, x1, x_min) - x_max = max(x0, x1, x_max) - y_min = min(y0, y1, y_min) - y_max = max(y0, y1, y_max) - x0 = x1 - y0 = y1 - return Area(x_min, y_min, x_max - x_min, y_max - y_min) - - -def blit(canvas, source, x, y, key=-1, palette=None): - """ - Blit a source to the canvas at the specified x, y location. - - Args: - source (FrameBuffer): Source FrameBuffer object. - x (int): X-coordinate to blit to. - y (int): Y-coordinate to blit to. - key (int): Key value for transparency (default: -1). - palette (Palette): Palette object for color translation (default: None). - - Returns: - (Area): The bounding box of the blitted area. - """ - fast = try_fast_framebuffer_blit(canvas, source, x, y, key, palette) - if fast is not None: - return fast - - from ._blit_hooks import clip_blit_bounds - - clipped = clip_blit_bounds(canvas, source, x, y) - if clipped is None: - return None - - x0, y0, w, h, src_x, src_y = clipped - x0end = x0 + w - y0end = y0 + h - - for cy0 in range(y0, y0end): - cx1 = src_x + (cy0 - y0) - src_row = src_y + (cy0 - y0) - for cx0 in range(x0, x0end): - col = source.pixel(cx1, src_row) - if palette: - col = palette.pixel(col, 0) - if col != key: - pixel(canvas, cx0, cy0, col) - cx1 += 1 - return Area(x0, y0, w, h) - - -def blit_rect(canvas, buf, x, y, w, h): - """ - Blit a rectangular area from a buffer to the canvas. Uses the canvas's blit_rect method if available, - otherwise writes directly to the buffer. - - Args: - buf (memoryview): Buffer to blit. Must already be byte-swapped if necessary. - x (int): X-coordinate to blit to. - y (int): Y-coordinate to blit to. - w (int): Width of the area to blit. - h (int): Height of the area to blit. - - Returns: - (Area): The bounding box of the blitted area. - """ - return blit_rect_dispatch(canvas, buf, x, y, w, h) - - -def blit_transparent(canvas, buf, x, y, w, h, key): - """ - Blit a buffer with transparency. - - Args: - buf (memoryview): Buffer to blit. - x (int): X-coordinate to blit to. - y (int): Y-coordinate to blit to. - w (int): Width of the area to blit. - h (int): Height of the area to blit. - key (int): Key value for transparency. - - Returns: - (Area): The bounding box of the blitted area. - """ - if canvas_accepts_blit_transparent(canvas): - canvas.blit_transparent(buf, x, y, w, h, key) - return Area(x, y, w, h) - - BPP = canvas.color_depth // 8 - key_bytes = key.to_bytes(BPP, "little") - stride = w * BPP - for j in range(h): - rowstart = j * stride - colstart = 0 - # iterate over each pixel looking for the first non-key pixel - while colstart < stride: - startoffset = rowstart + colstart - if buf[startoffset : startoffset + BPP] != key_bytes: - # found a non-key pixel - # then iterate over each pixel looking for the next key pixel - colend = colstart - while colend < stride: - endoffset = rowstart + colend - if buf[endoffset : endoffset + BPP] == key_bytes: - break - colend += BPP - # blit the non-key pixels - blit_rect( - canvas, - buf[rowstart + colstart : rowstart + colend], - x + colstart // BPP, - y + j, - (colend - colstart) // BPP, - 1, - ) - colstart = colend - else: - colstart += BPP - return Area(x, y, w, h) - - -def circle(canvas, x0, y0, r, c, f=False): - """ - Circle drawing function. Will draw a single pixel wide circle - centered at x0, y0 and the specified r. - - Args: - x0 (int): Center x coordinate - y0 (int): Center y coordinate - r (int): Radius - c (int): Color - f (bool): Fill the circle (default: False) - - Returns: - (Area): The bounding box of the circle. - """ - if f: - return _fill_circle_helper(canvas, x0, y0, r, c, 0, 0) - - _circle_helper(canvas, x0, y0, r, c, 0, 0) - return Area(x0 - r, y0 - r, 2 * r, 2 * r) - - -def _circle_helper(canvas, x0, y0, r, c, x_offset, y_offset): - """ - Circle helper function. Draws the 4 quadrants of a circle with center at x0, y0 and the specified r - separated by the specified x_offset and y_offset. Draws a circle if offsets are 0. Draws the 4 corners of - a round_rect if an offset is greater than 0. - """ - f = 1 - r - ddF_x = 1 - ddF_y = -2 * r - x = 0 - y = r - while x < y: - if f >= 0: - y -= 1 - ddF_y += 2 - f += ddF_y - x += 1 - ddF_x += 2 - f += ddF_x - offset_x = x + x_offset - offset_y = y + y_offset - pixel(canvas, x0 + offset_x - 1, y0 - offset_y, c) # 90 to 45 - pixel(canvas, x0 - offset_x, y0 - offset_y, c) # 90 to 135 - pixel(canvas, x0 + offset_x - 1, y0 + offset_y - 1, c) # 270 to 315 - pixel(canvas, x0 - offset_x, y0 + offset_y - 1, c) # 270 to 225 - offset_x = y + x_offset - offset_y = x + y_offset - pixel(canvas, x0 + offset_x - 1, y0 + offset_y - 1, c) # 0 to 315 - pixel(canvas, x0 - offset_x, y0 + offset_y - 1, c) # 180 to 225 - pixel(canvas, x0 + offset_x - 1, y0 - offset_y, c) # 0 to 45 - pixel(canvas, x0 - offset_x, y0 - offset_y, c) # 180 to 135 - - -def _fill_circle_helper(canvas, x0, y0, r, c, x_offset, y_offset): - """ - Fill circle helper function. Draws the 4 quadrants of a filled circle with center at x0, y0 and the - specified r separated by the specified x_offset and y_offset. Fills a circle if offsets are 0. Fills the - 4 corners of a filled round_rect if an offset is greater than 0. - """ - # vline(canvas, x0, y0 - r, 2 * r + 1, c) - f = 1 - r - ddF_x = 1 - ddF_y = -2 * r - x = 0 - y = r - while x < y: - if f >= 0: - y -= 1 - ddF_y += 2 - f += ddF_y - x += 1 - ddF_x += 2 - f += ddF_x - offset_x = x + x_offset - offset_y = y + y_offset - vline(canvas, x0 - offset_x, y0 - offset_y, 2 * offset_y, c) - vline(canvas, x0 + offset_x - 1, y0 - offset_y, 2 * offset_y, c) - offset_x = y + x_offset - offset_y = x + y_offset - vline(canvas, x0 - offset_x, y0 - offset_y, 2 * offset_y, c) - vline(canvas, x0 + offset_x - 1, y0 - offset_y, 2 * offset_y, c) - - return Area(x0 - r, y0 - r, 2 * r, 2 * r) - - -def ellipse(canvas, x0, y0, r1, r2, c, f=False, m=0b1111, w=None, h=None): - """ - Midpoint ellipse algorithm - Draw an ellipse at the given location. Radii r1 and r2 define the geometry; equal values cause a - circle to be drawn. The c parameter defines the color. - - The optional f parameter can be set to True to fill the ellipse. Otherwise just a one pixel outline - is drawn. - - The optional m parameter enables drawing to be restricted to certain quadrants of the ellipse. - The LS four bits determine which quadrants are to be drawn, with bit 0 specifying Q1, b1 Q2, - b2 Q3 and b3 Q4. Quadrants are numbered counterclockwise with Q1 being top right. - - Args: - x0 (int): Center x coordinate - y0 (int): Center y coordinate - r1 (int): x radius - r2 (int): y radius - c (int): color - f (bool): Fill the ellipse (default: False) - m (int): Bitmask to determine which quadrants to draw (default: 0b1111) - w (int): Width of the ellipse (default: None) - h (int): Height of the ellipse (default: None) - - Returns: - (Area): The bounding box of the ellipse. - """ - if r1 < 1 or r2 < 1: - return - - x_side = w - 2 * r1 if w else 0 - y_side = h - 2 * r2 if h else 0 - x_offset = x_side // 2 if w else 0 - y_offset = y_side // 2 if h else 0 - - if f: - if y_offset > 0: - fill_rect(canvas, x0 - w // 2, y0 - y_offset, w, y_side, c) - if x_offset > 0: - fill_rect(canvas, x0 - x_offset, y0 - h // 2, x_side, r1, c) - fill_rect(canvas, x0 - x_offset, y0 + h // 2 - r1, x_side, r1, c) - - if x_offset > 0: - hline(canvas, x0 - x_offset, y0 - h // 2, x_side, c) - hline(canvas, x0 - x_offset, y0 + h // 2, x_side, c) - if y_offset > 0: - vline(canvas, x0 - w // 2, y0 - y_offset, y_side, c) - vline(canvas, x0 + w // 2, y0 - y_offset, y_side, c) - - a2 = r1 * r1 - b2 = r2 * r2 - fa2 = 4 * a2 - fb2 = 4 * b2 - - x1 = r1 - y1 = 0 - sigma = 2 * a2 + b2 * (1 - 2 * r1) - while a2 * y1 <= b2 * x1: - if f: - if m & 0x1: - hline(canvas, x0 + x_offset, y0 - y1 - y_offset, x1, c) - if m & 0x2: - hline(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, x1, c) - if m & 0x4: - hline(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, x1, c) - if m & 0x8: - hline(canvas, x0 + x_offset, y0 + y1 + y_offset, x1, c) - else: - if m & 0x1: - pixel(canvas, x0 + x1 + x_offset, y0 - y1 - y_offset, c) - if m & 0x2: - pixel(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, c) - if m & 0x4: - pixel(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, c) - if m & 0x8: - pixel(canvas, x0 + x1 + x_offset, y0 + y1 + y_offset, c) - if sigma >= 0: - sigma += fb2 * (1 - x1) - x1 -= 1 - sigma += a2 * ((4 * y1) + 6) - y1 += 1 - - x1 = 0 - y1 = r2 - sigma = 2 * b2 + a2 * (1 - 2 * r2) - while b2 * x1 <= a2 * y1: - if f: - if m & 0x1: - hline(canvas, x0 + x_offset, y0 - y1 - y_offset, x1, c) - if m & 0x2: - hline(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, x1, c) - if m & 0x4: - hline(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, x1, c) - if m & 0x8: - hline(canvas, x0 + x_offset, y0 + y1 + y_offset, x1, c) - else: - if m & 0x1: - pixel(canvas, x0 + x1 + x_offset, y0 - y1 - y_offset, c) - if m & 0x2: - pixel(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, c) - if m & 0x4: - pixel(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, c) - if m & 0x8: - pixel(canvas, x0 + x1 + x_offset, y0 + y1 + y_offset, c) - if sigma >= 0: - sigma += fa2 * (1 - y1) - y1 -= 1 - sigma += b2 * ((4 * x1) + 6) - x1 += 1 - return Area(x0 - r1 - x_offset, y0 - r2 - y_offset, 2 * (r1 + x_offset), 2 * (r2 + y_offset)) - - -def fill(canvas, c): - """ - Fill the entire canvas with a color. Uses the canvas's fill method if available, - otherwise calls the fill_rect function. - - Args: - c (int): color. - - Returns: - (Area): The bounding box of the filled area. - """ - if hasattr(canvas, "fill"): - canvas.fill(c) - return Area(0, 0, canvas.width, canvas.height) - else: - return fill_rect(canvas, 0, 0, canvas.width, canvas.height, c) - - -def fill_rect(canvas, x, y, w, h, c): - """ - Filled rectangle drawing function. Draws a filled rectangle starting at - x, y and extending w, h pixels. Uses the canvas's fill_rect method if available, - otherwise calls the pixel function for each pixel. - - Args: - x (int): X-coordinate of the top-left corner of the rectangle. - y (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - c (int): color - - Returns: - (Area): The bounding box of the filled area. - """ - if y < -h or y > canvas.height or x < -w or x > canvas.width: - return - if hasattr(canvas, "fill_rect"): - canvas.fill_rect(x, y, w, h, c) - else: - for j in range(y, y + h): - for i in range(x, x + w): - pixel(canvas, i, j, c) - return Area(x, y, w, h) - - -def gradient_rect(canvas, x, y, w, h, c1, c2=None, vertical=True): - """ - Fill a rectangle with a gradient. - - Args: - x (int): X-coordinate of the top-left corner of the rectangle. - y (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - c1 (int): 565 encoded color for the top or left edge. - c2 (int): 565 encoded color for the bottom or right edge. If None or the same as c1, - fill_rect will be called instead. - vertical (bool): If True, the gradient will be vertical. If False, the gradient will be horizontal. - - Returns: - (Area): The bounding box of the filled area. - """ - if c2 is None or c1 == c2: - return fill_rect(canvas, x, y, w, h, c1) - r1, g1, b1 = (c1 >> 8) & 0xF8, (c1 >> 3) & 0xFC, (c1 << 3) & 0xF8 - r2, g2, b2 = (c2 >> 8) & 0xF8, (c2 >> 3) & 0xFC, (c2 << 3) & 0xF8 - if vertical: - for j in range(h): - r = r1 + (r2 - r1) * j // h - g = g1 + (g2 - g1) * j // h - b = b1 + (b2 - b1) * j // h - c = (r & 0xF8) << 8 | (g & 0xFC) << 3 | (b & 0xF8) >> 3 - fill_rect(canvas, x, y + j, w, 1, c) - else: - for i in range(w): - r = r1 + (r2 - r1) * i // w - g = g1 + (g2 - g1) * i // w - b = b1 + (b2 - b1) * i // w - c = (r & 0xF8) << 8 | (g & 0xFC) << 3 | (b & 0xF8) >> 3 - fill_rect(canvas, x + i, y, 1, h, c) - return Area(x, y, w, h) - - -def hline(canvas, x0, y0, w, c): - """ - Horizontal line drawing function. Will draw a single pixel wide line. - - Args: - x0 (int): X-coordinate of the start of the line. - y0 (int): Y-coordinate of the start of the line. - w (int): Width of the line. - c (int): color. - - Returns: - (Area): The bounding box of the line. - """ - if y0 < 0 or y0 > canvas.height or x0 < -w or x0 > canvas.width: - return - fill_rect(canvas, x0, y0, w, 1, c) - return Area(x0, y0, w, 1) - - -def line(canvas, x0, y0, x1, y1, c): - """ - Line drawing function. Will draw a single pixel wide line starting at - x0, y0 and ending at x1, y1. - - Args: - x0 (int): X-coordinate of the start of the line. - y0 (int): Y-coordinate of the start of the line. - x1 (int): X-coordinate of the end of the line. - y1 (int): Y-coordinate of the end of the line. - c (int): color. - - Returns: - (Area): The bounding box of the line. - """ - if x0 == x1: - return vline(canvas, x0, y0, abs(y1 - y0) + 1, c) - if y0 == y1: - return hline(canvas, x0, y0, abs(x1 - x0) + 1, c) - - steep = abs(y1 - y0) > abs(x1 - x0) - if steep: - x0, y0 = y0, x0 - x1, y1 = y1, x1 - if x0 > x1: - x0, x1 = x1, x0 - y0, y1 = y1, y0 - dx = x1 - x0 - dy = abs(y1 - y0) - err = dx // 2 - ystep = 0 - ystep = 1 if y0 < y1 else -1 - while x0 <= x1: - if steep: - pixel(canvas, y0, x0, c) - else: - pixel(canvas, x0, y0, c) - err -= dy - if err < 0: - y0 += ystep - err += dx - x0 += 1 - return Area(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) - - -def pixel(canvas, x, y, c): - """ - Draw a single pixel at the specified x, y location. Uses the canvas's pixel method if available, - otherwise writes directly to the buffer. - - Args: - x (int): X-coordinate of the pixel. - y (int): Y-coordinate of the pixel. - c (int): color. - - Returns: - (Area): The bounding box of the pixel. - """ - if hasattr(canvas, "pixel"): - canvas.pixel(x, y, c) - else: - rgb565_color = (c & 0xFFFF).to_bytes(2, "little") - canvas.buffer[(y * canvas.width + x) * 2 : (y * canvas.width + x) * 2 + 2] = rgb565_color - return Area(x, y, 1, 1) - - -def poly(canvas, x, y, coords, c, f=False): - """ - 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 an array of integers, e.g. array('h', [x0, y0, x1, y1, ... xn, yn]) or a - list or tuple of points, e.g. [(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. - - Args: - x (int): X-coordinate of the polygon's position. - y (int): Y-coordinate of the polygon's position. - coords (list): List of coordinates. - c (int): color. - f (bool): Fill the polygon (default: False). - - Returns: - (Area): The bounding box of the polygon. - """ - - # Convert the coords to a list of x, y tuples if it is not already - if isinstance(coords, list): - vertices = coords - elif isinstance(coords, tuple): - vertices = list(coords) - else: - # Check that the coords array has an even number of elements - if len(coords) % 2 != 0: - raise ValueError("coords must have an even number of elements") - vertices = [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] - - # Check that the polygon has at least 3 vertices - if len(vertices) < 3: - raise ValueError("polygon must have at least 3 vertices") - - # Close the polygon if it is not already closed - if vertices[0] != vertices[-1]: - vertices.append(vertices[0]) - - # Offset vertices by (x, y) - vertices = [(x + vertex[0], y + vertex[1]) for vertex in vertices] - - # Find the rectangle bounding box of the polygon - left = min(vertex[0] for vertex in vertices) - right = max(vertex[0] for vertex in vertices) - top = min(vertex[1] for vertex in vertices) - bottom = max(vertex[1] for vertex in vertices) - - if f: - # Fill the polygon using scanline algorithm - # Calculate the minimum and maximum y-coordinates in the polygon - y_min = min(vertex[1] for vertex in vertices) - y_max = max(vertex[1] for vertex in vertices) - - # Iterate through each y-coordinate within the bounding box - for y_scan in range(y_min, y_max + 1): - # Determine intersections with the polygon edges - intersections = [] - for i in range(len(vertices) - 1): - x1, y1 = vertices[i] - x2, y2 = vertices[i + 1] - # Check if the scanline intersects the edge - if y1 <= y_scan < y2 or y2 <= y_scan < y1: - # Calculate the intersection point using linear interpolation - x_intersection = x1 + ((y_scan - y1) / (y2 - y1)) * (x2 - x1) - intersections.append(x_intersection) - - # Sort intersections in increasing order - intersections.sort() - - # Draw horizontal lines between pairs of intersection points - for i in range(0, len(intersections), 2): - x_start = int(intersections[i]) - x_end = int(intersections[i + 1]) - hline(canvas, x_start, y_scan, x_end - x_start, c) - else: - for i in range(len(vertices) - 1): - line( - canvas, - vertices[i][0], - vertices[i][1], - vertices[i + 1][0], - vertices[i + 1][1], - c, - ) - return Area(left, top, right - left, bottom - top) - - -def polygon(canvas, points, x, y, color, angle=0, center_x=0, center_y=0): - """ - Draw a polygon on the canvas. - - Args: - points (list): List of points to draw. - x (int): X-coordinate of the polygon's position. - y (int): Y-coordinate of the polygon's position. - color (int): color. - angle (float): Rotation angle in radians (default: 0). - center_x (int): X-coordinate of the rotation center (default: 0). - center_y (int): Y-coordinate of the rotation center (default: 0). - - Raises: - ValueError: If the polygon has less than 3 points. - - Returns: - (Area): The bounding box of the polygon. - """ - # MIT License - # Copyright (c) 2024 Brad Barnett - # Copyright (c) 2020-2023 Russ Hughes - # Copyright (c) 2019 Ivan Belokobylskiy - if len(points) < 3: - raise ValueError("Polygon must have at least 3 points.") - - # fmt: off - if angle: - cos_a = math.cos(angle) - sin_a = math.sin(angle) - rotated = [ - (x + center_x + int((point[0] - center_x) * cos_a - (point[1] - center_y) * sin_a), - y + center_y + int((point[0] - center_x) * sin_a + (point[1] - center_y) * cos_a)) - for point in points - ] - else: - rotated = [(x + int((point[0])), y + int((point[1]))) for point in points] - - # Find the rectangle bounding box of the polygon - left = min(vertex[0] for vertex in rotated) - right = max(vertex[0] for vertex in rotated) - top = min(vertex[1] for vertex in rotated) - bottom = max(vertex[1] for vertex in rotated) - - for i in range(1, len(rotated)): - line(canvas, rotated[i - 1][0], rotated[i - 1][1], rotated[i][0], rotated[i][1], color) - # fmt: on - return Area(left, top, right - left, bottom - top) - - -def rect(canvas, x0, y0, w, h, c, f=False): - """ - Rectangle drawing function. Will draw a single pixel wide rectangle starting at - x0, y0 and extending w, h pixels. - - Args: - x0 (int): X-coordinate of the top-left corner of the rectangle. - y0 (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - c (int): color. - f (bool): Fill the rectangle (default: False). - - Returns: - (Area): The bounding box of the rectangle. - """ - if f: - return fill_rect(canvas, x0, y0, w, h, c) - if y0 < -h or y0 > canvas.height or x0 < -w or x0 > canvas.width: - return - hline(canvas, x0, y0, w, c) - hline(canvas, x0, y0 + h - 1, w, c) - vline(canvas, x0, y0, h, c) - vline(canvas, x0 + w - 1, y0, h, c) - return Area(x0, y0, w, h) - - -def round_rect(canvas, x0, y0, w, h, r, c, f=False): - """ - Rounded rectangle drawing function. Will draw a single pixel wide rounded rectangle starting at - x0, y0 and extending w, h pixels with the specified radius. - - Args: - x0 (int): X-coordinate of the top-left corner of the rectangle. - y0 (int): Y-coordinate of the top-left corner of the rectangle. - w (int): Width of the rectangle. - h (int): Height of the rectangle. - r (int): Radius of the corners. - c (int): color. - f (bool): Fill the rectangle (default: False). - - Returns: - (Area): The bounding box of the rectangle. - """ - # If the radius is 0, just draw a rectangle - if r == 0: - return rect(canvas, x0, y0, w, h, c, f) - - # If filled, draw the rounded rectangle using the _fill_round_rect function - if f: - return _fill_round_rect(canvas, x0, y0, w, h, r, c) - - # ensure that the r will only ever half of the shortest side or less - r = int(min(r, w / 2, h / 2)) - - hline(canvas, x0 + r, y0, w - 2 * r, c) # top - hline(canvas, x0 + r, y0 + h - 1, w - 2 * r, c) # bottom - vline(canvas, x0, y0 + r, h - 2 * r, c) # left - vline(canvas, x0 + w - 1, y0 + r, h - 2 * r, c) # right - _circle_helper(canvas, x0 + w // 2, y0 + h // 2, r, c, w // 2 - r, h // 2 - r) - return Area(x0, y0, w, h) - - -def _fill_round_rect(canvas, x0, y0, w, h, r, c): - """ - Filled rounded rectangle drawing function. Will draw a filled rounded rectangle starting at - x0, y0 and extending w, h pixels with the specified radius. - """ - - # ensure that the r will only ever be half of the shortest side or less - r = int(min(r, w / 2, h / 2)) - fill_rect(canvas, x0 + r, y0, w - 2 * r, h, c) # center - _fill_circle_helper(canvas, x0 + w // 2, y0 + h // 2, r, c, w // 2 - r, h // 2 - r) - return Area(x0, y0, w, h) - - -def triangle(canvas, x0, y0, x1, y1, x2, y2, c, f=False): - # pylint: disable=too-many-arguments - """ - Triangle drawing function. Draws a single pixel wide triangle with vertices at - (x0, y0), (x1, y1), and (x2, y2). - - Args: - x0 (int): X-coordinate of the first vertex. - y0 (int): Y-coordinate of the first vertex. - x1 (int): X-coordinate of the second vertex. - y1 (int): Y-coordinate of the second vertex. - x2 (int): X-coordinate of the third vertex. - y2 (int): Y-coordinate of the third vertex. - c (int): color. - f (bool): Fill the triangle (default: False). - - Returns: - (Area): The bounding box of the triangle. - """ - if f: - return _fill_triangle(canvas, x0, y0, x1, y1, x2, y2, c) - line(canvas, x0, y0, x1, y1, c) - line(canvas, x1, y1, x2, y2, c) - line(canvas, x2, y2, x0, y0, c) - left = min(x0, x1, x2) - top = min(y0, y1, y2) - right = max(x0, x1, x2) - bottom = max(y0, y1, y2) - return Area(left, top, right - left, bottom - top) - - -def _fill_triangle(canvas, x0, y0, x1, y1, x2, y2, c): - # pylint: disable=too-many-arguments - """ - Filled triangle drawing function. Will draw a filled triangle with vertices at - (x0, y0), (x1, y1), and (x2, y2). - """ - if y0 > y1: - y0, y1 = y1, y0 - x0, x1 = x1, x0 - if y1 > y2: - y2, y1 = y1, y2 - x2, x1 = x1, x2 - if y0 > y1: - y0, y1 = y1, y0 - x0, x1 = x1, x0 - a = 0 - b = 0 - last = 0 - if y0 == y2: - a = x0 - b = x0 - if x1 < a: - a = x1 - elif x1 > b: - b = x1 - if x2 < a: - a = x2 - elif x2 > b: - b = x2 - hline(canvas, a, y0, b - a + 1, c) - return - dx01 = x1 - x0 - dy01 = y1 - y0 - dx02 = x2 - x0 - dy02 = y2 - y0 - dx12 = x2 - x1 - dy12 = y2 - y1 - if dy01 == 0: - dy01 = 1 - if dy02 == 0: - dy02 = 1 - if dy12 == 0: - dy12 = 1 - sa = 0 - sb = 0 - y = y0 - last = y1 - 1 if y0 == y1 else y1 - while y <= last: - a = x0 + sa // dy01 - b = x0 + sb // dy02 - sa += dx01 - sb += dx02 - if a > b: - a, b = b, a - hline(canvas, a, y, b - a + 1, c) - y += 1 - sa = dx12 * (y - y1) - sb = dx02 * (y - y0) - while y <= y2: - a = x1 + sa // dy12 - b = x0 + sb // dy02 - sa += dx12 - sb += dx02 - if a > b: - a, b = b, a - hline(canvas, a, y, b - a + 1, c) - y += 1 - left = min(x0, x1, x2) - top = min(y0, y1, y2) - right = max(x0, x1, x2) - bottom = max(y0, y1, y2) - return Area(left, top, right - left, bottom - top) - - -def vline(canvas, x0, y0, h, c): - """ - Horizontal line drawing function. Will draw a single pixel wide line. - - Args: - x0 (int): X-coordinate of the start of the line. - y0 (int): Y-coordinate of the start of the line. - h (int): Height of the line. - c (int): color. - - Returns: - (Area): The bounding box of the line. - """ - if y0 < -h or y0 > canvas.height or x0 < 0 or x0 > canvas.width: - return - fill_rect(canvas, x0, y0, 1, h, c) - return Area(x0, y0, 1, h) diff --git a/tests/README.md b/tests/README.md index 581049e44..e4f18afe9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,7 @@ # Tests Self-contained tests for in-tree [`multimer`](../src/lib/multimer), -[`eventsys`](../src/lib/eventsys), [`graphics`](../src/lib/graphics), and +[`eventsys`](../src/lib/eventsys), [`graphics`](https://github.com/PyDevices/graphics) (sibling `lib/graphics`), and [`displaysys`](../src/lib/displaysys) packages. They use only the Python standard library (`unittest`) — no third-party test diff --git a/tests/_env.py b/tests/_env.py index 53a382741..f716f8a10 100644 --- a/tests/_env.py +++ b/tests/_env.py @@ -3,15 +3,13 @@ # SPDX-License-Identifier: MIT """Shared test bootstrap that puts pydisplay packages on ``sys.path``. -The tests are self-contained: importing this module makes ``src/lib`` (where -``eventsys``, ``graphics``, ``displaysys``, and ``multimer`` live) importable -without installing anything. Import it -before importing those packages in any test module:: +Puts ``src/lib`` (displaysys, eventsys, multimer) and ``src/add_ons`` on +``sys.path`` without installing anything. Does **not** require sibling git +checkouts or optional packages such as ``graphics`` / ``usdl2``. import _env # noqa: F401 import multimer import eventsys - import graphics import displaysys """ @@ -27,44 +25,18 @@ if _SRC_ADDONS not in sys.path: sys.path.insert(0, _SRC_ADDONS) - -def _sync_graphics_framebuf(): - """Materialize ``graphics/framebuf.py`` from ``add_ons/framebuf.py`` (single source).""" - import subprocess - - sync = os.path.join(_REPO_ROOT, "scripts", "install_sync_framebuf.py") - subprocess.run([sys.executable, sync], cwd=_REPO_ROOT, check=True) - - -_sync_graphics_framebuf() - #: Absolute path to the ``multimer`` package directory. MULTIMER_DIR = os.path.join(_SRC_LIB, "multimer") -#: Absolute path to the ``eventsys`` package directory, handy for tests that -#: want to copy it somewhere isolated. +#: Absolute path to the ``eventsys`` package directory. EVENTSYS_DIR = os.path.join(_SRC_LIB, "eventsys") -#: Absolute path to the ``graphics`` package directory, handy for tests that -#: want to copy it somewhere isolated. -GRAPHICS_DIR = os.path.join(_SRC_LIB, "graphics") - -#: Absolute path to the ``displaysys`` package directory, handy for tests that -#: want to copy it somewhere isolated. +#: Absolute path to the ``displaysys`` package directory. DISPLAYSYS_DIR = os.path.join(_SRC_LIB, "displaysys") -_DRIVERS_DISPLAY = os.path.join(_REPO_ROOT, "drivers", "display") -_DRIVERS_BUS = os.path.join(_REPO_ROOT, "drivers", "bus") -_DRIVERS_TOUCH = os.path.join(_REPO_ROOT, "drivers", "touch") -_DRIVERS_INPUT = os.path.join(_REPO_ROOT, "drivers", "input") - -for _path in (_DRIVERS_DISPLAY, _DRIVERS_BUS, _DRIVERS_TOUCH, _DRIVERS_INPUT): - if _path not in sys.path: - sys.path.insert(0, _path) - def _ensure_micropython_shim(): - """CPython unit tests import MCU-oriented drivers that expect ``micropython``.""" + """CPython unit tests may import MCU-oriented helpers that expect ``micropython``.""" if "micropython" in sys.modules: return import types diff --git a/tests/_support.py b/tests/_support.py index c28d84f99..63f0f2594 100644 --- a/tests/_support.py +++ b/tests/_support.py @@ -1,15 +1,13 @@ # SPDX-FileCopyrightText: 2026 Brad Barnett # # SPDX-License-Identifier: MIT -"""Helpers shared by the multimer, eventsys, graphics, and displaysys tests.""" +"""Helpers shared by the multimer, eventsys, and displaysys tests.""" import contextlib import io import _env # noqa: F401 -import graphics - def scripted(*values): """Return a zero-arg callable that yields ``values`` one poll at a time. @@ -45,26 +43,6 @@ def __init__(self, width=320, height=240, rotation=0): self.touch_device = None -def make_fb(format=graphics.RGB565, w=16, h=16): - """Return a fresh :class:`graphics.FrameBuffer` of the given format. - - The backing buffer is always allocated at ``w * h * 2`` bytes, which is - large enough for every supported color depth (1-16 bits per pixel), so the - same helper works for mono, grayscale and RGB565 framebuffers. - """ - buffer = bytearray(w * h * 2) - return graphics.FrameBuffer(buffer, w, h, format) - - -def set_pixels(fb): - """Return the set of ``(x, y)`` coordinates whose pixel value is non-zero.""" - return {(x, y) for y in range(fb.height) for x in range(fb.width) if fb.pixel(x, y)} - - -def count_set(fb): - """Return how many pixels in ``fb`` have a non-zero value.""" - return len(set_pixels(fb)) - class FakeFrameBuffer: """A minimal stand-in for a CircuitPython ``FrameBuffer``. diff --git a/tests/test_area.py b/tests/test_area.py deleted file mode 100644 index 01b149052..000000000 --- a/tests/test_area.py +++ /dev/null @@ -1,107 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for the ``graphics.Area`` rectangle helper.""" - -import unittest - -import _env # noqa: F401 - -from graphics import Area - - -class TestAreaConstruction(unittest.TestCase): - def test_positional_args(self): - a = Area(1, 2, 3, 4) - self.assertEqual((a.x, a.y, a.w, a.h), (1, 2, 3, 4)) - - def test_tuple_arg(self): - a = Area((5, 6, 7, 8)) - self.assertEqual((a.x, a.y, a.w, a.h), (5, 6, 7, 8)) - - def test_missing_args_raises(self): - with self.assertRaises(ValueError): - Area(1) - with self.assertRaises(ValueError): - Area(1, 2) - - -class TestAreaContainment(unittest.TestCase): - def setUp(self): - self.a = Area(0, 0, 10, 10) - - def test_contains_point(self): - self.assertTrue(self.a.contains(5, 5)) - self.assertTrue(self.a.contains(0, 0)) - - def test_contains_is_half_open(self): - # The far edges are exclusive. - self.assertFalse(self.a.contains(10, 10)) - self.assertFalse(self.a.contains(-1, 5)) - - def test_contains_tuple(self): - self.assertTrue(self.a.contains((3, 4))) - - def test_contains_area(self): - self.assertTrue(self.a.contains_area(Area(1, 1, 2, 2))) - self.assertFalse(self.a.contains_area(Area(0, 0, 20, 20))) - - -class TestAreaOverlap(unittest.TestCase): - def test_intersects(self): - self.assertTrue(Area(0, 0, 5, 5).intersects(Area(4, 4, 5, 5))) - - def test_edge_touch_does_not_intersect(self): - # Sharing only an edge is not an intersection. - self.assertFalse(Area(0, 0, 5, 5).intersects(Area(5, 5, 2, 2))) - - def test_disjoint_does_not_intersect(self): - self.assertFalse(Area(0, 0, 5, 5).intersects(Area(10, 10, 2, 2))) - - def test_edge_touch_counts_as_touch(self): - self.assertTrue(Area(0, 0, 5, 5).touches_or_intersects(Area(5, 5, 2, 2))) - - def test_gap_does_not_touch(self): - self.assertFalse(Area(0, 0, 5, 5).touches_or_intersects(Area(6, 6, 2, 2))) - - -class TestAreaTransforms(unittest.TestCase): - def test_shift(self): - self.assertEqual(Area(1, 2, 3, 4).shift(10, 20), Area(11, 22, 3, 4)) - - def test_clip(self): - self.assertEqual(Area(0, 0, 10, 10).clip(Area(5, 5, 10, 10)), Area(5, 5, 5, 5)) - - def test_offset_uniform(self): - self.assertEqual(Area(2, 2, 4, 4).offset(1), Area(1, 1, 6, 6)) - - def test_offset_x_y(self): - self.assertEqual(Area(2, 2, 4, 4).offset(1, 2), Area(1, 0, 6, 8)) - - def test_inset_uniform(self): - self.assertEqual(Area(2, 2, 4, 4).inset(1), Area(3, 3, 2, 2)) - - def test_union_via_add(self): - self.assertEqual(Area(0, 0, 2, 2) + Area(4, 4, 2, 2), Area(0, 0, 6, 6)) - - -class TestAreaProtocols(unittest.TestCase): - def test_eq_and_ne(self): - self.assertEqual(Area(1, 2, 3, 4), Area(1, 2, 3, 4)) - self.assertNotEqual(Area(1, 2, 3, 4), Area(1, 2, 3, 5)) - - def test_iterable_unpacks_to_xywh(self): - x, y, w, h = Area(1, 2, 3, 4) - self.assertEqual((x, y, w, h), (1, 2, 3, 4)) - - def test_repr(self): - self.assertEqual(repr(Area(1, 2, 3, 4)), "Area(1, 2, 3, 4)") - - def test_is_unhashable(self): - # Area defines __eq__ but sets __hash__ = None on purpose. - with self.assertRaises(TypeError): - hash(Area(1, 2, 3, 4)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_blit_hooks.py b/tests/test_blit_hooks.py deleted file mode 100644 index c515e5546..000000000 --- a/tests/test_blit_hooks.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for graphics blit hook dispatch.""" - -import unittest - -import _env # noqa: F401 -from _support import make_fb - -from graphics import Area, Draw, blit, blit_rect -from graphics._blit_hooks import ( - blit_rect_dispatch, - canvas_accepts_blit_rect, - canvas_accepts_blit_transparent, - try_fast_framebuffer_blit, -) - - -class _BlitRectCanvas: - color_depth = 16 - - def __init__(self, width, height): - self.width = width - self.height = height - self.calls = [] - - def blit_rect(self, buf, x, y, w, h): - self.calls.append((bytes(buf), x, y, w, h)) - - -class _BlitTransparentCanvas(_BlitRectCanvas): - def __init__(self, width, height): - super().__init__(width, height) - self.transparent_calls = [] - - def blit_transparent(self, buf, x, y, w, h, key): - self.transparent_calls.append((bytes(buf), x, y, w, h, key)) - - -class TestBlitHooks(unittest.TestCase): - def test_framebuffer_rejects_rect_hook(self): - fb = make_fb(w=8, h=8) - self.assertFalse(canvas_accepts_blit_rect(fb)) - self.assertFalse(canvas_accepts_blit_transparent(fb)) - - def test_display_like_canvas_accepts_hooks(self): - canvas = _BlitRectCanvas(16, 16) - self.assertTrue(canvas_accepts_blit_rect(canvas)) - canvas = _BlitTransparentCanvas(16, 16) - self.assertTrue(canvas_accepts_blit_transparent(canvas)) - - def test_draw_blit_uses_display_blit_rect(self): - src = make_fb(w=4, h=4) - src.fill(0xBEEF) - display = _BlitRectCanvas(16, 16) - draw = Draw(display) - area = draw.blit(src, 2, 2) - self.assertEqual(area, Area(2, 2, 4, 4)) - self.assertEqual(len(display.calls), 1) - _buf, x, y, w, h = display.calls[0] - self.assertEqual((x, y, w, h), (2, 2, 4, 4)) - self.assertEqual(len(_buf), 4 * 4 * 2) - - def test_blit_rect_dispatch_to_display(self): - canvas = _BlitRectCanvas(8, 8) - buf = bytearray(2 * 2 * 2) - blit_rect_dispatch(canvas, buf, 1, 1, 2, 2) - self.assertEqual(len(canvas.calls), 1) - - def test_blit_rect_dispatch_to_framebuffer(self): - dst = make_fb(w=8, h=8) - dst.fill(0) - buf = bytearray([0x34, 0x12, 0x78, 0x56]) - blit_rect(dst, buf, 1, 1, 2, 1) - self.assertEqual(dst.pixel(1, 1), 0x1234) - self.assertEqual(dst.pixel(2, 1), 0x5678) - - def test_try_fast_framebuffer_blit_clips(self): - src = make_fb(w=4, h=4) - src.fill(0x00FF) - dst = make_fb(w=8, h=8) - dst.fill(0) - area = try_fast_framebuffer_blit(dst, src, 6, 6) - self.assertEqual(area, Area(6, 6, 2, 2)) - self.assertEqual(dst.pixel(6, 6), 0x00FF) - self.assertEqual(dst.pixel(7, 7), 0x00FF) - self.assertEqual(dst.pixel(5, 5), 0) - - def test_blit_transparent_uses_display_hook(self): - from graphics import blit_transparent - - canvas = _BlitTransparentCanvas(8, 8) - buf = bytearray(2 * 2 * 2) - blit_transparent(canvas, buf, 0, 0, 2, 2, 0) - self.assertEqual(len(canvas.transparent_calls), 1) - - def test_framebuffer_blit_rect_no_recursion(self): - dst = make_fb(w=8, h=8) - buf = bytearray(2 * 2 * 2) - area = dst.blit_rect(buf, 0, 0, 2, 2) - self.assertEqual(area, Area(0, 0, 2, 2)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_clip.py b/tests/test_clip.py deleted file mode 100644 index ec7db10bb..000000000 --- a/tests/test_clip.py +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for clip-region helpers.""" - -import unittest - -import _env # noqa: F401 -from _support import make_fb - -from graphics import Area -from graphics._clip import ClippedCanvas, crop_rgb565_buffer, intersect_rect - - -class TestClipHelpers(unittest.TestCase): - def test_intersect_rect(self): - self.assertEqual( - intersect_rect(0, 0, 10, 10, Area(5, 5, 10, 10)), - Area(5, 5, 5, 5), - ) - self.assertIsNone(intersect_rect(0, 0, 2, 2, Area(10, 10, 2, 2))) - - def test_crop_rgb565_buffer(self): - buf = bytearray([0, 1, 2, 3, 4, 5, 6, 7]) - cropped = crop_rgb565_buffer(buf, 2, 1, 0, 1, 2) - self.assertEqual(bytes(cropped), bytes([2, 3, 6, 7])) - - def test_clipped_canvas_fill_rect(self): - fb = make_fb(w=8, h=8) - fb.fill(0) - clipped = ClippedCanvas(fb, Area(2, 2, 3, 3)) - clipped.fill_rect(0, 0, 8, 8, 0xFFFF) - self.assertEqual(fb.pixel(1, 1), 0) - self.assertEqual(fb.pixel(2, 2), 0xFFFF) - self.assertEqual(fb.pixel(4, 4), 0xFFFF) - self.assertEqual(fb.pixel(5, 5), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_contract_proof.py b/tests/test_contract_proof.py deleted file mode 100644 index ca14feaa8..000000000 --- a/tests/test_contract_proof.py +++ /dev/null @@ -1,200 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Structural smoke tests for graduated boards in micropython-hardware.""" - -from __future__ import annotations - -import ast -import importlib.util -import json -from pathlib import Path -import sys -import unittest - -ROOT = Path(__file__).resolve().parents[1] -HW = ROOT.parent / "micropython-hardware" -LIB = ROOT / "src" / "lib" - -# Graduated campaign boards: name -> path under micropython-hardware/board_configs/ -EXPECTED = { - "esp32-p4-wifi6-touch-lcd-4b": { - "path": "fbdisplay/esp32-p4-wifi6-touch-lcd-4b", - "eager_touch": True, - "devices": { - "audio", - "microphone", - "sdcard", - "camera", - "ethernet", - "radio", - "wlan", - "ble", - "usb_device", - }, - }, - "qualia_tl040hds20": { - "path": "fbdisplay/qualia_tl040hds20", - "eager_touch": True, - "eager_keypad": True, - "devices": {"wlan", "ble"}, - }, - "esp32-s3-touch-lcd-4_3": { - "path": "fbdisplay/esp32-s3-touch-lcd-4_3", - "eager_touch": True, - "devices": {"sdcard", "can", "rs485", "usb_device", "wlan", "ble"}, - }, - "esp32-s3-touch-lcd-7": { - "path": "fbdisplay/esp32-s3-touch-lcd-7", - "eager_touch": True, - "devices": {"sdcard", "can", "rs485", "usb_device", "wlan", "ble"}, - }, - "t-rgb_480": { - "path": "fbdisplay/t-rgb_480", - "eager_touch": True, - "devices": {"sdcard", "battery", "wlan", "ble"}, - }, - "t-embed": { - "path": "busdisplay/spi/t-embed", - "eager_encoder": True, - "devices": { - "pixels", - "audio", - "microphone", - "sdcard", - "battery", - "i2c", - "wlan", - "ble", - }, - }, - "t-hmi": { - "path": "busdisplay/i80/t-hmi", - "eager_touch": True, - "devices": {"sdcard", "i2c", "wlan", "ble"}, - }, - "rp2040-touch-lcd-1.28": { - "path": "busdisplay/spi/rp2040-touch-lcd-1.28", - "eager_touch": True, - "devices": {"accelerometer", "gyroscope", "battery"}, - }, - "metro_m7_tft_touch_shield_1947": { - "path": "busdisplay/spi/metro_m7_tft_touch_shield_1947", - "eager_touch": True, - "devices": {"pixels", "led", "sdcard", "radio", "wlan", "i2c"}, - }, - "nucleo_h743zi2_tft_touch_shield_1947": { - "path": "busdisplay/spi/nucleo_h743zi2_tft_touch_shield_1947", - "eager_touch": True, - "devices": {"led", "sdcard", "ethernet"}, - }, -} - - -def _parse(path: Path) -> ast.Module: - return ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - - -def _has_call(tree: ast.AST, name: str) -> bool: - for node in ast.walk(tree): - if isinstance(node, ast.Call): - func = node.func - if isinstance(func, ast.Name) and func.id == name: - return True - if isinstance(func, ast.Attribute) and func.attr == name: - return True - return False - - -def _assigned_names(tree: ast.AST) -> set[str]: - names: set[str] = set() - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - for t in node.targets: - if isinstance(t, ast.Name): - names.add(t.id) - return names - - -@unittest.skipUnless(HW.is_dir(), "sibling micropython-hardware clone required") -class TestGraduatedBoardLayout(unittest.TestCase): - def test_each_board_files_and_ast(self): - for name, expect in EXPECTED.items(): - with self.subTest(board=name): - d = HW / "board_configs" / expect["path"] - bc = d / "board_config.py" - bd = d / "board_devices.py" - pkg = d / "package.json" - self.assertTrue(bc.is_file(), bc) - self.assertTrue(bd.is_file(), bd) - self.assertTrue(pkg.is_file(), pkg) - - bc_tree = _parse(bc) - names = _assigned_names(bc_tree) - - self.assertIn("display_drv", names) - self.assertIn("runtime", names) - self.assertTrue(_has_call(bc_tree, "setup_devices")) - if expect.get("eager_touch"): - self.assertIn("touch", names) - if "runtime = None" not in bc.read_text(): - self.assertIn("touch_read=", bc.read_text()) - if expect.get("eager_encoder"): - self.assertIn("encoder", names) - self.assertTrue(_has_call(bc_tree, "add_encoder")) - if expect.get("eager_keypad"): - self.assertTrue(_has_call(bc_tree, "add_keypad")) - - text = bd.read_text() - self.assertIn("DEVICES = frozenset", text) - self.assertIn("def setup_devices", text) - for role in expect["devices"]: - self.assertIn("def {}(".format(role), text) - - meta = json.loads(pkg.read_text()) - urls = {u[0] for u in meta["urls"]} - self.assertIn("board_config.py", urls) - self.assertIn("board_devices.py", urls) - self.assertIn("boarddev.py", urls) - joined = " ".join(u[1] for u in meta["urls"]) - self.assertIn("micropython-hardware/", joined) - self.assertIn("pydisplay/src/lib/boarddev.py", joined) - - -@unittest.skipUnless(HW.is_dir(), "sibling micropython-hardware clone required") -class TestGraduatedBindLazy(unittest.TestCase): - def test_bind_lazy_and_devices_membership(self): - if str(LIB) not in sys.path: - sys.path.insert(0, str(LIB)) - import boarddev # noqa: F401 - - for name, expect in EXPECTED.items(): - with self.subTest(board=name): - path = HW / "board_configs" / expect["path"] / "board_devices.py" - mod_name = "hw_{}_devices".format(name.replace("-", "_")) - spec = importlib.util.spec_from_file_location(mod_name, path) - mod = importlib.util.module_from_spec(spec) - sys.modules[mod_name] = mod - assert spec.loader is not None - spec.loader.exec_module(mod) - - self.assertEqual(set(mod.DEVICES), expect["devices"]) - ns = {"display_drv": object(), "runtime": object()} - mod.setup_devices(ns) - self.assertIn("__getattr__", ns) - - with self.assertRaises(AttributeError): - ns["__getattr__"]("not_a_role") - - for role in sorted(mod.DEVICES): - ns.pop(role, None) - try: - obj = ns["__getattr__"](role) - except (NotImplementedError, ImportError, OSError): - continue - else: - self.assertIs(ns[role], obj) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_draw.py b/tests/test_draw.py deleted file mode 100644 index be9a526c4..000000000 --- a/tests/test_draw.py +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for the ``graphics.Draw`` convenience wrapper. - -``Draw`` binds a canvas once and forwards each call to ``graphics._shapes`` / -``graphics._font``, returning the ``Area`` bounding box. -""" - -import unittest - -import _env # noqa: F401 -from _support import count_set, make_fb - -from graphics import Area, Draw - -_WHITE = 0xFFFF - - -class TestDraw(unittest.TestCase): - def setUp(self): - self.canvas = make_fb(w=16, h=16) - self.canvas.fill(0) - self.draw = Draw(self.canvas) - - def test_binds_canvas(self): - self.assertIs(self.draw.canvas, self.canvas) - - def test_fill(self): - self.assertEqual(self.draw.fill(_WHITE), Area(0, 0, 16, 16)) - self.assertEqual(count_set(self.canvas), 16 * 16) - - def test_fill_rect(self): - self.assertEqual(self.draw.fill_rect(2, 2, 3, 3, _WHITE), Area(2, 2, 3, 3)) - self.assertEqual(count_set(self.canvas), 9) - - def test_pixel(self): - self.assertEqual(self.draw.pixel(4, 5, _WHITE), Area(4, 5, 1, 1)) - self.assertEqual(self.canvas.pixel(4, 5), _WHITE) - - def test_hline(self): - self.assertEqual(self.draw.hline(0, 0, 4, _WHITE), Area(0, 0, 4, 1)) - - def test_rect(self): - self.assertEqual(self.draw.rect(1, 1, 5, 5, _WHITE), Area(1, 1, 5, 5)) - - def test_circle(self): - self.assertEqual(self.draw.circle(8, 8, 3, _WHITE), Area(5, 5, 6, 6)) - self.assertGreater(count_set(self.canvas), 0) - - def test_line(self): - self.assertIsInstance(self.draw.line(0, 0, 5, 5, _WHITE), Area) - - def test_text(self): - self.assertIsInstance(self.draw.text("Hi", 0, 0, _WHITE), Area) - self.assertGreater(count_set(self.canvas), 0) - - def test_blit_transparent(self): - buf = bytearray(2 * 2 * 2) - buf[0:2] = (0xFF, 0xFF) - buf[2:4] = (0x00, 0x00) - area = self.draw.blit_transparent(buf, 0, 0, 2, 2, key=0) - self.assertEqual(area, Area(0, 0, 2, 2)) - - def test_clip_fill_rect(self): - with self.draw.clip(2, 2, 4, 4): - self.draw.fill_rect(0, 0, 16, 16, _WHITE) - self.assertEqual(self.canvas.pixel(1, 1), 0) - self.assertEqual(self.canvas.pixel(2, 2), _WHITE) - self.assertEqual(self.canvas.pixel(5, 5), _WHITE) - self.assertEqual(self.canvas.pixel(6, 6), 0) - - def test_clip_pixel_outside_is_noop(self): - with self.draw.clip(4, 4, 2, 2): - self.draw.pixel(0, 0, _WHITE) - self.draw.pixel(4, 4, _WHITE) - self.assertEqual(self.canvas.pixel(0, 0), 0) - self.assertEqual(self.canvas.pixel(4, 4), _WHITE) - - def test_nested_clip_intersects(self): - with self.draw.clip(0, 0, 8, 8), self.draw.clip(4, 4, 8, 8): - self.draw.fill_rect(0, 0, 16, 16, _WHITE) - self.assertEqual(self.canvas.pixel(3, 3), 0) - self.assertEqual(self.canvas.pixel(4, 4), _WHITE) - self.assertEqual(self.canvas.pixel(7, 7), _WHITE) - self.assertEqual(self.canvas.pixel(8, 8), 0) - - def test_clip_restored_after_context(self): - with self.draw.clip(2, 2, 2, 2): - self.draw.fill_rect(2, 2, 2, 2, _WHITE) - self.draw.pixel(0, 0, _WHITE) - self.assertEqual(self.canvas.pixel(0, 0), _WHITE) - - def test_clip_fill_only_region(self): - self.canvas.fill(0) - with self.draw.clip(1, 1, 3, 3): - self.draw.fill(_WHITE) - self.assertEqual(self.canvas.pixel(0, 0), 0) - self.assertEqual(self.canvas.pixel(2, 2), _WHITE) - self.assertEqual(self.canvas.pixel(4, 4), 0) - - def test_clip_accepts_area(self): - with self.draw.clip(Area(2, 2, 2, 2)): - self.draw.fill_rect(0, 0, 16, 16, _WHITE) - self.assertEqual(self.canvas.pixel(2, 2), _WHITE) - self.assertEqual(self.canvas.pixel(4, 4), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_epaperdisplay_chip.py b/tests/test_epaperdisplay_chip.py deleted file mode 100644 index d944df06a..000000000 --- a/tests/test_epaperdisplay_chip.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``epaperdisplay_chip`` sequence parsing.""" - -import unittest - -from epaperdisplay_chip import _send_command_sequence - - -class _RecordingBus: - def __init__(self): - self.calls = [] - - def send(self, command, data=None): - self.calls.append((command, data or b"")) - - -class TestEpaperChipSequence(unittest.TestCase): - def test_two_byte_length_soft_reset_delay(self): - bus = _RecordingBus() - _send_command_sequence(bus, b"\x12\x80\x00\x14", two_byte_sequence_length=True) - self.assertEqual(bus.calls[0][0], 0x12) - self.assertEqual(bus.calls[0][1], b"") - - def test_refresh_command_single_byte(self): - bus = _RecordingBus() - _send_command_sequence(bus, b"\x20\x00") - self.assertEqual(bus.calls, [(0x20, b"")]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_eventsys_quit.py b/tests/test_eventsys_quit.py index 472da987c..a07353d73 100644 --- a/tests/test_eventsys_quit.py +++ b/tests/test_eventsys_quit.py @@ -39,21 +39,6 @@ def test_none_runtime_poll_is_noop(self): runtime = Runtime() self.assertFalse(runtime.quit_requested) - def test_quit_sets_flag(self): - class Display: - needs_refresh = False - - def quit(self): - pass - - runtime = Runtime(display=Display()) - host = HostEventsDevice( - host_read=scripted([events.Quit(events.QUIT)]), - display=Display(), - ) - runtime.register(host) - runtime.poll() - self.assertTrue(runtime.quit_requested) class TestRuntimeQuitLifecycle(unittest.TestCase): diff --git a/tests/test_files.py b/tests/test_files.py deleted file mode 100644 index aa311e597..000000000 --- a/tests/test_files.py +++ /dev/null @@ -1,222 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``graphics`` image file I/O (save / from_file and the converters). - -Covers the formats whose pure-Python read/write paths round-trip cleanly on -CPython: PBM (MONO_HLSB), PGM (grayscale), and BMP (RGB565). -""" - -import os -import struct -import tempfile -import unittest - -import _env # noqa: F401 - -from graphics import ( - BMP565, - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_VLSB, - RGB565, - FrameBuffer, - bmp_to_framebuffer, - load_image, - pbm_to_framebuffer, - pgm_to_framebuffer, - save_image, -) - - -class _TmpDirTest(unittest.TestCase): - def setUp(self): - self._dir = tempfile.mkdtemp(prefix="graphics_files_") - - def tearDown(self): - import shutil - - shutil.rmtree(self._dir, ignore_errors=True) - - def _path(self, name): - return os.path.join(self._dir, name) - - -class TestSaveLoadRoundTrip(_TmpDirTest): - def test_pbm_roundtrip(self): - fb = FrameBuffer(bytearray((16 + 7) // 8 * 8), 16, 8, MONO_HLSB) - fb.fill(0) - fb.fill_rect(1, 1, 3, 3, 1) - path = self._path("img.pbm") - fb.save(path) - - loaded = FrameBuffer.from_file(path) - self.assertEqual(loaded.width, 16) - self.assertEqual(loaded.height, 8) - self.assertEqual(loaded.format, MONO_HLSB) - self.assertEqual(bytes(loaded.buffer), bytes(fb.buffer)) - self.assertEqual(loaded.pixel(2, 2), 1) - - def test_pgm_roundtrip(self): - # Build via per-pixel writes (GS2 fill() would convert the buffer to a - # list, which save cannot serialise). - fb = FrameBuffer(bytearray((16 + 3) // 4 * 8), 16, 8, GS2_HMSB) - fb.fill_rect(1, 1, 3, 3, 3) - path = self._path("img.pgm") - fb.save(path) - - loaded = FrameBuffer.from_file(path) - self.assertEqual(loaded.width, 16) - self.assertEqual(loaded.height, 8) - self.assertEqual(loaded.format, GS2_HMSB) - self.assertEqual(bytes(loaded.buffer), bytes(fb.buffer)) - self.assertEqual(loaded.pixel(2, 2), 3) - - def test_save_appends_extension(self): - fb = FrameBuffer(bytearray((8 + 7) // 8 * 8), 8, 8, MONO_HLSB) - fb.fill(0) - fb.save(self._path("noext")) - self.assertTrue(os.path.exists(self._path("noext.pbm"))) - - def test_rgb565_saves_bmp_signature(self): - fb = FrameBuffer(bytearray(8 * 8 * 2), 8, 8, RGB565) - fb.fill(0x1234) - path = self._path("img.bmp") - fb.save(path) - with open(path, "rb") as f: - self.assertEqual(f.read(2), b"BM") - - def test_gs4_roundtrip(self): - buf = bytearray((8 + 1) // 2 * 8) - buf[0] = 0x1E - buf[4] = 0xF0 - fb = FrameBuffer(buf, 8, 8, GS4_HMSB) - path = self._path("img.pgm") - fb.save(path) - loaded = FrameBuffer.from_file(path) - self.assertEqual(loaded.format, GS4_HMSB) - self.assertEqual(bytes(loaded.buffer), bytes(fb.buffer)) - - def test_gs8_roundtrip(self): - buf = bytearray(8 * 8) - buf[10] = 200 - buf[63] = 42 - fb = FrameBuffer(buf, 8, 8, GS8) - path = self._path("img.pgm") - save_image(fb, path) - loaded = load_image(path) - self.assertEqual(loaded.format, GS8) - self.assertEqual(bytes(loaded.buffer), bytes(fb.buffer)) - - def test_rgb565_roundtrip(self): - fb = FrameBuffer(bytearray(4 * 4 * 2), 4, 4, RGB565) - fb.pixel(1, 2, 0xBEEF) - path = self._path("img.bmp") - fb.save(path) - loaded = load_image(path) - self.assertEqual(loaded.format, RGB565) - self.assertEqual(loaded.pixel(1, 2), 0xBEEF) - self.assertEqual(bytes(loaded.buffer), bytes(fb.buffer)) - - -class TestConverters(_TmpDirTest): - def test_pbm_to_framebuffer(self): - path = self._path("hand.pbm") - with open(path, "wb") as f: - f.write(b"P4\n2 2\n") - f.write(bytes([0b11000000, 0b00000000])) # 1 byte per row, 2 rows - fb = pbm_to_framebuffer(path) - self.assertEqual((fb.width, fb.height), (2, 2)) - self.assertEqual(fb.format, MONO_HLSB) - self.assertEqual(fb.pixel(0, 0), 1) - self.assertEqual(fb.pixel(0, 1), 0) - - def test_pgm_to_framebuffer_gs8(self): - path = self._path("hand.pgm") - with open(path, "wb") as f: - f.write(b"P5\n2 2\n255\n") - f.write(bytes([10, 20, 30, 40])) - fb = pgm_to_framebuffer(path) - self.assertEqual((fb.width, fb.height), (2, 2)) - self.assertEqual(fb.format, GS8) - self.assertEqual(fb.pixel(0, 0), 10) - self.assertEqual(fb.pixel(1, 1), 40) - - def test_pgm_to_framebuffer_gs4(self): - path = self._path("hand.pgm") - with open(path, "wb") as f: - f.write(b"P5\n2 2\n15\n") - f.write(bytes([0x1E, 0x00])) - fb = pgm_to_framebuffer(path) - self.assertEqual((fb.width, fb.height), (2, 2)) - self.assertEqual(fb.format, GS4_HMSB) - self.assertEqual(bytes(fb.buffer), b"\x1e\x00") - - def test_save_unsupported_format_raises(self): - fb = FrameBuffer(bytearray(8), 8, 8, MONO_VLSB) - with self.assertRaises(ValueError): - save_image(fb, self._path("out.pbm")) - - def test_pbm_bad_magic_raises(self): - path = self._path("bad.pbm") - with open(path, "wb") as f: - f.write(b"XX\n1 1\n\x00") - with self.assertRaises(ValueError): - pbm_to_framebuffer(path) - - -class TestBmp565(_TmpDirTest): - def _write_minimal_bmp(self, path, width, height, pixels): - from graphics._bmp565 import write_bmp565_file - - with open(path, "wb") as f: - write_bmp565_file(f, pixels, width, height) - - def test_bmp_to_framebuffer(self): - path = self._path("tiny.bmp") - pixels = bytearray([0x34, 0x12, 0x78, 0x56]) # two RGB565 pixels, top row - self._write_minimal_bmp(path, 2, 1, pixels) - fb = bmp_to_framebuffer(path) - self.assertEqual((fb.width, fb.height), (2, 1)) - self.assertEqual(fb.format, RGB565) - self.assertEqual(fb.pixel(0, 0), 0x1234) - self.assertEqual(fb.pixel(1, 0), 0x5678) - - def test_bmp565_roundtrip(self): - path = self._path("asset.bmp") - pixels = bytearray(4 * 2 * 2) - for i in range(4): - struct.pack_into(" height 8. - data = memoryview(bytearray(256 * 8)) - f = Font(data, 8) - self.assertEqual(f.height, 8) - self.assertEqual(f.font_name, "memoryview") - - -class TestTextHelpers(unittest.TestCase): - def setUp(self): - self.fb = make_fb(w=64, h=32) - self.fb.fill(0) - - def test_text8_returns_area(self): - self.assertEqual(text8(self.fb, "Hi", 0, 0, 0xFFFF), Area(0, 0, 16, 8)) - - def test_text14_returns_area(self): - self.assertEqual(text14(self.fb, "Hi", 0, 0, 0xFFFF), Area(0, 0, 16, 14)) - - def test_text16_returns_area(self): - self.assertEqual(text16(self.fb, "Hi", 0, 0, 0xFFFF), Area(0, 0, 16, 16)) - - def test_text_actually_draws_pixels(self): - text8(self.fb, "A", 0, 0, 0xFFFF) - self.assertGreater(count_set(self.fb), 0) - - def test_text_blank_string_draws_nothing(self): - text8(self.fb, " ", 0, 0, 0xFFFF) - self.assertEqual(count_set(self.fb), 0) - - def test_newline_advances_a_row(self): - bbox = text8(self.fb, "A\nB", 0, 0, 0xFFFF) - # two lines of an 8px-high font - self.assertEqual(bbox.h, 16) - - -class TestTextSelector(unittest.TestCase): - def setUp(self): - self.fb = make_fb(w=64, h=32) - self.fb.fill(0) - - def test_height_dispatch(self): - self.assertEqual(text(self.fb, "X", 0, 0, 0xFFFF, height=8).h, 8) - self.assertEqual(text(self.fb, "X", 0, 0, 0xFFFF, height=14).h, 14) - self.assertEqual(text(self.fb, "X", 0, 0, 0xFFFF, height=16).h, 16) - - def test_unsupported_height_raises(self): - with self.assertRaises(ValueError): - text(self.fb, "X", 0, 0, 0xFFFF, height=13) - - def test_missing_font_file_raises(self): - with self.assertRaises(FileNotFoundError): - Font("/nonexistent/font8x8.bin", 8) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_framebuf.py b/tests/test_framebuf.py deleted file mode 100644 index 27f0e762f..000000000 --- a/tests/test_framebuf.py +++ /dev/null @@ -1,372 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for the pure-Python ``framebuf`` module (Damien-parity, MP-pure). - -This mirrors MicroPython's ``extmod/modframebuf.c`` behavior on CPython: every -drawing method returns ``None`` (like the compiled MicroPython module), not a -tuple or ``Area``. ``graphics._framebuf_plus`` wraps this base class to add -``Area`` bounding boxes and pydisplay extensions. -""" - -import unittest - -import _env # noqa: F401 -from framebuf import ( - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_HMSB, - MONO_VLSB, - MVLSB, - RGB565, - FrameBuffer, -) - -# Formats whose pure-Python get/set round-trip cleanly on CPython. -_ROUNDTRIP_FORMATS = (MONO_VLSB, MONO_HLSB, MONO_HMSB, RGB565, GS2_HMSB, GS4_HMSB, GS8) - - -def _fb(format, w=16, h=16): - return FrameBuffer(bytearray(w * h * 3 + 64), w, h, format) - - -class TestFrameBufferBasics(unittest.TestCase): - def test_invalid_format_raises(self): - with self.assertRaises(ValueError): - FrameBuffer(bytearray(16), 4, 4, 99) - - def test_invalid_dimensions_raise(self): - with self.assertRaises(ValueError): - FrameBuffer(bytearray(16), 0, 4, RGB565) - with self.assertRaises(ValueError): - FrameBuffer(bytearray(16), 4, 0, RGB565) - - def test_mvlsb_alias(self): - self.assertEqual(MVLSB, MONO_VLSB) - - def test_width_height_properties(self): - fb = _fb(RGB565, 12, 7) - self.assertEqual(fb.width, 12) - self.assertEqual(fb.height, 7) - - def test_pixel_set_returns_none(self): - fb = _fb(RGB565) - result = fb.pixel(1, 1, 0x1234) - self.assertIsNone(result) - - def test_pixel_out_of_bounds_returns_none(self): - fb = _fb(RGB565, 8, 8) - self.assertIsNone(fb.pixel(-1, 0)) - self.assertIsNone(fb.pixel(8, 0)) - self.assertIsNone(fb.pixel(0, 8)) - - def test_pixel_set_out_of_bounds_is_noop(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0) - # Out-of-bounds set must not raise and must not touch the buffer. - fb.pixel(-1, 0, 0xFFFF) - fb.pixel(8, 0, 0xFFFF) - fb.pixel(0, 8, 0xFFFF) - self.assertTrue(all(b == 0 for b in fb._buffer[: 8 * 8 * 2])) - - -class TestFrameBufferPixelRoundTrip(unittest.TestCase): - def test_pixel_roundtrip(self): - for fmt in _ROUNDTRIP_FORMATS: - with self.subTest(format=fmt): - fb = _fb(fmt) - self.assertEqual(fb.pixel(3, 4), 0) - fb.pixel(3, 4, 1) - self.assertEqual(fb.pixel(3, 4), 1) - # An untouched neighbour stays clear. - self.assertEqual(fb.pixel(4, 4), 0) - - def test_rgb565_preserves_16bit_value(self): - fb = _fb(RGB565) - fb.pixel(2, 2, 0xBEEF) - self.assertEqual(fb.pixel(2, 2), 0xBEEF) - - def test_gs4_nibble_packing(self): - fb = _fb(GS4_HMSB, 8, 8) - fb.pixel(0, 0, 0x3) - fb.pixel(1, 0, 0xC) - self.assertEqual(fb.pixel(0, 0), 0x3) - self.assertEqual(fb.pixel(1, 0), 0xC) - - def test_gs8_roundtrip(self): - fb = _fb(GS8, 8, 8) - fb.pixel(3, 3, 0xAB) - self.assertEqual(fb.pixel(3, 3), 0xAB) - self.assertEqual(fb.pixel(2, 3), 0) - - -class TestFrameBufferFill(unittest.TestCase): - def test_fill_sets_every_pixel(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0x07E0) - self.assertTrue(all(fb.pixel(x, y) == 0x07E0 for x in range(8) for y in range(8))) - - def test_fill_returns_none(self): - fb = _fb(RGB565, 8, 8) - self.assertIsNone(fb.fill(0)) - - def test_fill_rect_returns_none(self): - fb = _fb(RGB565, 8, 8) - self.assertIsNone(fb.fill_rect(0, 0, 1, 1, 0)) - - def test_fill_rect_region(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0) - fb.fill_rect(2, 2, 3, 3, 0xFFFF) - self.assertEqual(fb.pixel(2, 2), 0xFFFF) - self.assertEqual(fb.pixel(4, 4), 0xFFFF) - self.assertEqual(fb.pixel(5, 5), 0) - self.assertEqual(fb.pixel(1, 1), 0) - - def test_fill_rect_clips_to_bounds(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0) - # Fully out of range: no-op, no exception. - fb.fill_rect(-10, -10, 2, 2, 0xFFFF) - fb.fill_rect(20, 20, 2, 2, 0xFFFF) - # Partially out of range: clipped, no exception. - fb.fill_rect(-2, -2, 4, 4, 0xFFFF) - self.assertEqual(fb.pixel(0, 0), 0xFFFF) - fb.fill_rect(6, 6, 4, 4, 0xFFFF) - self.assertEqual(fb.pixel(7, 7), 0xFFFF) - - def test_fill_rect_gs4_matches_pixel_loop(self): - fb = _fb(GS4_HMSB, 8, 8) - fb.fill(0) - fb.fill_rect(1, 1, 3, 3, 0xA) - for x in range(1, 4): - for y in range(1, 4): - self.assertEqual(fb.pixel(x, y), 0xA) - self.assertEqual(fb.pixel(0, 0), 0) - self.assertEqual(fb.pixel(4, 4), 0) - - -class TestFrameBufferShapes(unittest.TestCase): - def setUp(self): - self.fb = _fb(RGB565, 16, 16) - self.fb.fill(0) - - def test_hline_returns_none(self): - self.assertIsNone(self.fb.hline(0, 0, 5, 0xFFFF)) - self.assertTrue(all(self.fb.pixel(x, 0) == 0xFFFF for x in range(5))) - - def test_vline_returns_none(self): - self.assertIsNone(self.fb.vline(0, 0, 5, 0xFFFF)) - self.assertTrue(all(self.fb.pixel(0, y) == 0xFFFF for y in range(5))) - - def test_rect_outline_is_hollow(self): - self.assertIsNone(self.fb.rect(2, 2, 6, 6, 0xFFFF)) - self.assertEqual(self.fb.pixel(2, 2), 0xFFFF) - self.assertEqual(self.fb.pixel(7, 7), 0xFFFF) - self.assertEqual(self.fb.pixel(4, 4), 0) - - def test_rect_filled(self): - self.fb.rect(2, 2, 6, 6, 0xFFFF, True) - self.assertEqual(self.fb.pixel(4, 4), 0xFFFF) - - def test_line_returns_none(self): - self.assertIsNone(self.fb.line(0, 0, 5, 5, 0xFFFF)) - self.assertEqual(self.fb.pixel(0, 0), 0xFFFF) - self.assertEqual(self.fb.pixel(5, 5), 0xFFFF) - - def test_line_horizontal(self): - self.fb.line(1, 1, 6, 1, 0xFFFF) - self.assertTrue(all(self.fb.pixel(x, 1) == 0xFFFF for x in range(1, 7))) - - def test_ellipse_returns_none(self): - self.assertIsNone(self.fb.ellipse(8, 8, 3, 3, 0xFFFF)) - # A circle of radius 3 touches the point directly right of center. - self.assertEqual(self.fb.pixel(11, 8), 0xFFFF) - - def test_ellipse_filled_center_set(self): - self.fb.ellipse(8, 8, 3, 3, 0xFFFF, True) - self.assertEqual(self.fb.pixel(8, 8), 0xFFFF) - - def test_ellipse_zero_radius_sets_one_pixel(self): - self.fb.ellipse(5, 5, 0, 0, 0xFFFF) - self.assertEqual(self.fb.pixel(5, 5), 0xFFFF) - - def test_poly_outline_flat_list(self): - self.assertIsNone(self.fb.poly(0, 0, [2, 2, 8, 2, 8, 8, 2, 8], 0xFFFF)) - self.assertEqual(self.fb.pixel(2, 2), 0xFFFF) - self.assertEqual(self.fb.pixel(8, 2), 0xFFFF) - - def test_poly_outline_tuple_pairs(self): - self.fb.poly(0, 0, [(2, 2), (8, 2), (8, 8), (2, 8)], 0xFFFF) - self.assertEqual(self.fb.pixel(2, 2), 0xFFFF) - - def test_poly_filled_interior(self): - self.fb.poly(0, 0, [2, 2, 12, 2, 12, 12, 2, 12], 0xFFFF, True) - self.assertEqual(self.fb.pixel(6, 6), 0xFFFF) - self.assertEqual(self.fb.pixel(0, 0), 0) - - -class TestFrameBufferScroll(unittest.TestCase): - def test_scroll_moves_pixels(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0) - fb.pixel(0, 0, 0xFFFF) - self.assertIsNone(fb.scroll(2, 3)) - self.assertEqual(fb.pixel(2, 3), 0xFFFF) - - def test_scroll_supports_mono_vlsb(self): - # MicroPython's scroll works pixel-by-pixel for any depth, including - # sub-byte MONO_VLSB -- unlike a byte-copy shortcut, it must not raise. - fb = _fb(MONO_VLSB, 8, 8) - fb.fill(0) - fb.pixel(1, 1, 1) - fb.scroll(1, 1) - self.assertEqual(fb.pixel(2, 2), 1) - - def test_scroll_large_step_is_noop(self): - fb = _fb(RGB565, 8, 8) - fb.fill(0) - fb.pixel(0, 0, 0xFFFF) - fb.scroll(100, 0) - # Step >= width is a no-op per MicroPython semantics. - self.assertEqual(fb.pixel(0, 0), 0xFFFF) - - -class TestFrameBufferBlit(unittest.TestCase): - def test_blit_copies_pixels(self): - dst = _fb(RGB565, 8, 8) - dst.fill(0) - src = _fb(RGB565, 4, 4) - src.fill(0xABCD) - self.assertIsNone(dst.blit(src, 2, 2)) - self.assertEqual(dst.pixel(2, 2), 0xABCD) - self.assertEqual(dst.pixel(5, 5), 0xABCD) - self.assertEqual(dst.pixel(6, 6), 0) - - def test_blit_key_transparency(self): - dst = _fb(RGB565, 8, 8) - dst.fill(0x1111) - src = _fb(RGB565, 2, 2) - src.fill(0xABCD) - src.pixel(0, 0, 0x2222) - dst.blit(src, 0, 0, key=0xABCD) - self.assertEqual(dst.pixel(0, 0), 0x2222) - self.assertEqual(dst.pixel(1, 1), 0x1111) # keyed out, unchanged - - def test_blit_accepts_readonly_tuple_source(self): - dst = _fb(RGB565, 8, 8) - dst.fill(0) - buf = bytearray(4 * 4 * 2) - RGB565Buf = FrameBuffer(buf, 4, 4, RGB565) - RGB565Buf.fill(0x4321) - dst.blit((buf, 4, 4, RGB565), 1, 1) - self.assertEqual(dst.pixel(1, 1), 0x4321) - - def test_blit_out_of_bounds_is_noop(self): - dst = _fb(RGB565, 8, 8) - dst.fill(0) - src = _fb(RGB565, 4, 4) - src.fill(0xFFFF) - self.assertIsNone(dst.blit(src, 100, 100)) - self.assertTrue(all(b == 0 for b in dst._buffer)) - - -class TestFrameBufferText(unittest.TestCase): - def test_text_returns_none(self): - fb = _fb(RGB565, 32, 16) - fb.fill(0) - self.assertIsNone(fb.text("A", 0, 0, 0xFFFF)) - # 'A' glyph has at least one lit pixel in its 8x8 cell. - self.assertTrue(any(fb.pixel(x, y) == 0xFFFF for x in range(8) for y in range(8))) - - def test_text_default_color_is_one(self): - fb = _fb(MONO_HLSB, 32, 16) - fb.fill(0) - fb.text("A", 0, 0) - self.assertTrue(any(fb.pixel(x, y) == 1 for x in range(8) for y in range(8))) - - def test_text_out_of_range_char_uses_fallback_glyph(self): - fb = _fb(RGB565, 32, 16) - fb.fill(0) - fb.text(chr(200), 0, 0, 0xFFFF) - # Falls back to glyph 127, which is a checkerboard pattern (non-blank). - self.assertTrue(any(fb.pixel(x, y) == 0xFFFF for x in range(8) for y in range(8))) - - def test_text_columns_advance_without_gaps(self): - fb = _fb(RGB565, 32, 16) - fb.fill(0) - fb.text("II", 0, 0, 0xFFFF) - # Two 8px-wide glyphs are placed with zero gap: second glyph starts at x=8. - col0_lit = any(fb.pixel(x, y) == 0xFFFF for x in range(8) for y in range(8)) - col1_lit = any(fb.pixel(x, y) == 0xFFFF for x in range(8, 16) for y in range(8)) - self.assertTrue(col0_lit) - self.assertTrue(col1_lit) - - -class TestFormatsMatchReference(unittest.TestCase): - """Regression tests for bit-packing bugs found while auditing against modframebuf.c.""" - - def test_mhmsb_pixel_bit_order(self): - # MONO_HMSB: bit 0 of each byte is the *leftmost* pixel of its 8-wide group. - fb = _fb(MONO_HMSB, 8, 8) - fb.fill(0) - fb.pixel(0, 0, 1) - self.assertEqual(fb._buffer[0] & 0x01, 0x01) - self.assertEqual(fb.pixel(0, 0), 1) - self.assertEqual(fb.pixel(1, 0), 0) - - def test_mhlsb_pixel_bit_order(self): - # MONO_HLSB: bit 0 of each byte is the *rightmost* pixel of its 8-wide group. - fb = _fb(MONO_HLSB, 8, 8) - fb.fill(0) - fb.pixel(7, 0, 1) - self.assertEqual(fb._buffer[0] & 0x01, 0x01) - self.assertEqual(fb.pixel(7, 0), 1) - self.assertEqual(fb.pixel(6, 0), 0) - - def test_mhlsb_fill_rect_matches_pixel_loop(self): - fb_a = _fb(MONO_HLSB, 16, 16) - fb_b = _fb(MONO_HLSB, 16, 16) - fb_a.fill(0) - fb_b.fill(0) - fb_a.fill_rect(3, 2, 5, 4, 1) - for x in range(3, 8): - for y in range(2, 6): - fb_b.pixel(x, y, 1) - self.assertEqual(bytes(fb_a._buffer), bytes(fb_b._buffer)) - - def test_mhmsb_fill_rect_matches_pixel_loop(self): - fb_a = _fb(MONO_HMSB, 16, 16) - fb_b = _fb(MONO_HMSB, 16, 16) - fb_a.fill(0) - fb_b.fill(0) - fb_a.fill_rect(3, 2, 5, 4, 1) - for x in range(3, 8): - for y in range(2, 6): - fb_b.pixel(x, y, 1) - self.assertEqual(bytes(fb_a._buffer), bytes(fb_b._buffer)) - - def test_gs2_fill_preserves_buffer_type(self): - fb = _fb(GS2_HMSB, 8, 8) - fb.fill(3) - self.assertIsInstance(fb._buffer, bytearray) - self.assertEqual(fb.pixel(0, 0), 3) - - def test_gs8_fill_preserves_buffer_identity(self): - fb = _fb(GS8, 8, 8) - buf = fb._buffer - fb.fill(5) - self.assertIs(fb._buffer, buf) - self.assertEqual(fb.pixel(0, 0), 5) - - def test_mono_vlsb_stride_defaults_to_width(self): - # MicroPython does not round MONO_VLSB stride to a byte boundary (only - # height is padded internally); stride == width unless overridden. - fb = _fb(MONO_VLSB, 5, 8) - self.assertEqual(fb._stride, 5) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_framebuf_plus.py b/tests/test_framebuf_plus.py deleted file mode 100644 index 72d8297f6..000000000 --- a/tests/test_framebuf_plus.py +++ /dev/null @@ -1,127 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``graphics.FrameBuffer`` (the ``_framebuf_plus`` wrapper). - -This is the ``FrameBuffer`` exported from the package. Unlike the bare -``framebuf`` base class, every drawing method returns an :class:`graphics.Area` -bounding box and the buffer/format/color_depth are exposed as properties. -""" - -import unittest - -import _env # noqa: F401 -from _support import make_fb - -import graphics -from graphics import ( - GS2_HMSB, - GS4_HMSB, - GS8, - MONO_HLSB, - MONO_HMSB, - MONO_VLSB, - RGB565, - Area, - FrameBuffer, -) - -_DEPTHS = { - MONO_VLSB: 1, - MONO_HLSB: 1, - MONO_HMSB: 1, - RGB565: 16, - GS2_HMSB: 2, - GS4_HMSB: 4, - GS8: 8, -} - - -class TestProperties(unittest.TestCase): - def test_dimensions_and_format(self): - buffer = bytearray(20 * 10 * 2) - fb = FrameBuffer(buffer, 20, 10, RGB565) - self.assertEqual(fb.width, 20) - self.assertEqual(fb.height, 10) - self.assertEqual(fb.format, RGB565) - self.assertIs(fb.buffer, buffer) - - def test_color_depth_for_each_format(self): - for fmt, depth in _DEPTHS.items(): - with self.subTest(format=fmt): - fb = make_fb(fmt, 8, 8) - self.assertEqual(fb.color_depth, depth) - - def test_invalid_format_raises(self): - with self.assertRaises(ValueError): - FrameBuffer(bytearray(64), 4, 4, 99) - - -class TestMethodsReturnArea(unittest.TestCase): - def setUp(self): - self.fb = make_fb(RGB565, 16, 16) - - def test_fill_returns_area(self): - self.assertEqual(self.fb.fill(0), Area(0, 0, 16, 16)) - - def test_fill_rect_returns_area(self): - self.assertEqual(self.fb.fill_rect(2, 3, 4, 5, 0xFFFF), Area(2, 3, 4, 5)) - - def test_pixel_set_returns_area(self): - self.assertEqual(self.fb.pixel(1, 1, 0xFFFF), Area(1, 1, 1, 1)) - - def test_pixel_get_returns_int(self): - self.fb.pixel(1, 1, 0x1234) - self.assertEqual(self.fb.pixel(1, 1), 0x1234) - - def test_hline_returns_area(self): - self.assertEqual(self.fb.hline(0, 0, 5, 0xFFFF), Area(0, 0, 5, 1)) - - def test_vline_returns_area(self): - self.assertEqual(self.fb.vline(0, 0, 5, 0xFFFF), Area(0, 0, 1, 5)) - - def test_rect_returns_area(self): - self.assertEqual(self.fb.rect(1, 1, 5, 6, 0xFFFF), Area(1, 1, 5, 6)) - - def test_line_returns_area(self): - self.assertIsInstance(self.fb.line(0, 0, 5, 5, 0xFFFF), Area) - - def test_ellipse_returns_area(self): - self.assertEqual(self.fb.ellipse(8, 8, 3, 2, 0xFFFF), Area(5, 6, 6, 4)) - - def test_blit_returns_area(self): - src = make_fb(RGB565, 4, 4) - src.fill(0xABCD) - self.assertEqual(self.fb.blit(src, 2, 2), Area(2, 2, 4, 4)) - self.assertEqual(self.fb.pixel(2, 2), 0xABCD) - - def test_scroll_returns_area(self): - self.fb.fill_rect(0, 0, 4, 4, 0xFFFF) - self.assertEqual(self.fb.scroll(1, 0), Area(0, 0, 16, 16)) - - -class TestDrawingEffects(unittest.TestCase): - def setUp(self): - self.fb = make_fb(RGB565, 16, 16) - self.fb.fill(0) - - def test_fill_rect_sets_interior_only(self): - self.fb.fill_rect(4, 4, 3, 3, 0xFFFF) - self.assertEqual(self.fb.pixel(4, 4), 0xFFFF) - self.assertEqual(self.fb.pixel(6, 6), 0xFFFF) - self.assertEqual(self.fb.pixel(7, 7), 0) - - def test_rect_outline_is_hollow(self): - self.fb.rect(2, 2, 6, 6, 0xFFFF) - # corners drawn, centre untouched - self.assertEqual(self.fb.pixel(2, 2), 0xFFFF) - self.assertEqual(self.fb.pixel(7, 7), 0xFFFF) - self.assertEqual(self.fb.pixel(4, 4), 0) - - def test_rect_filled(self): - self.fb.rect(2, 2, 6, 6, 0xFFFF, True) - self.assertEqual(self.fb.pixel(4, 4), 0xFFFF) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_framebuf_sync.py b/tests/test_framebuf_sync.py deleted file mode 100644 index 763f0a8d6..000000000 --- a/tests/test_framebuf_sync.py +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Ensure the generated graphics.framebuf copy matches add_ons/framebuf.py.""" - -from pathlib import Path -import subprocess -import sys -import unittest - -import _env # noqa: F401 - - -class TestFramebufSync(unittest.TestCase): - def test_generated_graphics_framebuf_matches_canonical(self): - repo = Path(__file__).resolve().parents[1] - proc = subprocess.run( - [sys.executable, "scripts/install_sync_framebuf.py", "--check"], - cwd=repo, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual( - proc.returncode, - 0, - msg=proc.stderr or proc.stdout or "sync_framebuf --check failed", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_i2cbus.py b/tests/test_i2cbus.py deleted file mode 100644 index 255811bb3..000000000 --- a/tests/test_i2cbus.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``i2cbus.I2CBus``.""" - -import unittest - -import _env # noqa: F401 - - -class _FakeI2C: - def __init__(self): - self.writes = [] - - def writeto(self, address, buf): - self.writes.append((address, bytes(buf))) - - -class TestI2CBus(unittest.TestCase): - def test_send_command_only(self): - from i2cbus import I2CBus - - i2c = _FakeI2C() - bus = I2CBus(i2c, address=0x3C) - bus.send(0xAF) - self.assertEqual(i2c.writes, [(0x3C, bytes([0x00, 0xAF]))]) - - def test_send_command_with_data(self): - from i2cbus import I2CBus - - i2c = _FakeI2C() - bus = I2CBus(i2c) - bus.send(0x21, b"\x01\x02") - self.assertEqual(i2c.writes[0][1], bytes([0x00, 0x21, 0x01, 0x02])) - - def test_send_data(self): - from i2cbus import I2CBus - - i2c = _FakeI2C() - bus = I2CBus(i2c) - bus.send_data(b"\xAA\xBB") - self.assertEqual(i2c.writes[0][1], bytes([0x40, 0xAA, 0xBB])) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_keypad_shift.py b/tests/test_keypad_shift.py deleted file mode 100644 index 752cde7bc..000000000 --- a/tests/test_keypad_shift.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``keypad_shift.ShiftRegisterButtons``.""" - -import sys -import types -import unittest -from unittest import mock - -import _env # noqa: F401 - -try: - from eventsys.keys import Keys -except ImportError: - from keys import Keys - - -class _FakePin: - IN = 0 - OUT = 1 - PULL_UP = 2 - - def __init__(self, *_args, **_kwargs): - pass - - def value(self, val=None): - return 0 - - def init(self, mode, pull=None): - return None - - -class TestShiftRegisterButtons(unittest.TestCase): - def test_read_uses_bit_mapping(self): - machine = types.ModuleType("machine") - machine.Pin = _FakePin - with mock.patch.dict(sys.modules, {"machine": machine}): - from keypad_shift import ShiftRegisterButtons - - buttons = ShiftRegisterButtons( - clock=1, - latch=2, - data=3, - mapping={"a": (0, Keys.K_a), "b": (1, Keys.K_b)}, - value_when_pressed=False, - ) - buttons._read_bits = lambda: [0, 1, 1, 1, 1, 1, 1, 1] - self.assertEqual(buttons.read(), [Keys.K_a]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_pixeldisplay.py b/tests/test_pixeldisplay.py deleted file mode 100644 index d5d82e95d..000000000 --- a/tests/test_pixeldisplay.py +++ /dev/null @@ -1,116 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for displaysys.pixeldisplay.""" - -import unittest - -import _env # noqa: F401 - -from displaysys.pixeldisplay import ( - HORIZONTAL, - PixelDisplay, - PixelFramebuffer, - _build_grid_mapper, -) - - -def _horizontal_index(x, y, width, alternating): - if alternating and y % 2: - return y * width + (width - 1 - x) - return y * width + x - - -class MockStrip: - def __init__(self, count): - self.count = count - self.data = [(0, 0, 0)] * count - self.show_calls = 0 - - def __setitem__(self, index, value): - self.data[index] = value - - def show(self): - self.show_calls += 1 - - -class TestPixelGridMap(unittest.TestCase): - def test_8x8_alternating_horizontal(self): - _, _, indices = _build_grid_mapper(8, 8, orientation=HORIZONTAL, alternating=True) - self.assertEqual(len(indices), 64) - for y in range(8): - for x in range(8): - self.assertEqual( - indices[y * 8 + x], - _horizontal_index(x, y, 8, True), - ) - - def test_12x6_not_alternating_horizontal(self): - _, _, indices = _build_grid_mapper(12, 6, orientation=HORIZONTAL, alternating=False) - self.assertEqual(len(indices), 72) - for y in range(6): - for x in range(12): - self.assertEqual(indices[y * 12 + x], y * 12 + x) - - -class TestPixelFramebuffer(unittest.TestCase): - def test_display_writes_changed_pixels(self): - strip = MockStrip(32) - fb = PixelFramebuffer(strip, 8, 4, alternating=False) - fb.fill(0xFF0000) - fb.display() - self.assertEqual(strip.show_calls, 1) - self.assertEqual(strip.data[0], (255, 0, 0)) - self.assertEqual(strip.data[31], (255, 0, 0)) - - fb.display() - self.assertEqual(strip.show_calls, 2) - - fb.pixel(0, 0, 0x0000FF) - fb.display() - self.assertEqual(strip.data[0], (0, 0, 255)) - self.assertEqual(strip.data[1], (255, 0, 0)) - - -class TestPixelDisplay(unittest.TestCase): - def test_show_calls_display(self): - class Framebuf: - width = 4 - height = 4 - rotation = 0 - display_calls = 0 - - def display(self): - Framebuf.display_calls += 1 - - buf = Framebuf() - drv = PixelDisplay(buf) - drv.show() - self.assertEqual(Framebuf.display_calls, 1) - - def test_color_depth_is_16(self): - strip = MockStrip(32) - fb = PixelFramebuffer(strip, 8, 4, alternating=False) - drv = PixelDisplay(fb) - self.assertEqual(drv.color_depth, 16) - - def test_fill_rect_converts_565_to_strip_rgb(self): - strip = MockStrip(32) - fb = PixelFramebuffer(strip, 8, 4, alternating=False) - drv = PixelDisplay(fb) - drv.fill_rect(0, 0, 1, 1, 0xFFFF) - drv.show() - self.assertEqual(strip.data[0], (255, 255, 255)) - - def test_blit_rect_converts_565_buffer(self): - strip = MockStrip(32) - fb = PixelFramebuffer(strip, 8, 4, alternating=False) - drv = PixelDisplay(fb) - buf = bytes((0x00, 0xF8)) # 565 red, little-endian - drv.blit_rect(buf, 1, 0, 1, 1) - drv.show() - self.assertEqual(strip.data[1], (255, 0, 0)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_shapes.py b/tests/test_shapes.py deleted file mode 100644 index 3eb9464d2..000000000 --- a/tests/test_shapes.py +++ /dev/null @@ -1,178 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for the module-level drawing primitives in ``graphics._shapes``. - -These are the functions re-exported from the package top level (``hline``, -``rect``, ``circle`` ...). They take the canvas as the first argument and -return an :class:`graphics.Area` bounding box. -""" - -from array import array -import unittest - -import _env # noqa: F401 -from _support import count_set, make_fb - -from graphics import ( - Area, - arc, - blit, - circle, - ellipse, - fill, - fill_rect, - gradient_rect, - hline, - line, - pixel, - poly, - polygon, - rect, - round_rect, - triangle, - vline, -) - -_WHITE = 0xFFFF - - -class TestPrimitiveLines(unittest.TestCase): - def setUp(self): - self.c = make_fb(w=16, h=16) - self.c.fill(0) - - def test_pixel(self): - self.assertEqual(pixel(self.c, 3, 4, _WHITE), Area(3, 4, 1, 1)) - self.assertEqual(self.c.pixel(3, 4), _WHITE) - - def test_hline(self): - self.assertEqual(hline(self.c, 1, 2, 5, _WHITE), Area(1, 2, 5, 1)) - self.assertTrue(all(self.c.pixel(x, 2) == _WHITE for x in range(1, 6))) - self.assertEqual(self.c.pixel(6, 2), 0) - - def test_vline(self): - self.assertEqual(vline(self.c, 1, 2, 5, _WHITE), Area(1, 2, 1, 5)) - self.assertTrue(all(self.c.pixel(1, y) == _WHITE for y in range(2, 7))) - - def test_line_diagonal_marks_endpoints(self): - bbox = line(self.c, 0, 0, 5, 5, _WHITE) - self.assertIsInstance(bbox, Area) - self.assertEqual(self.c.pixel(0, 0), _WHITE) - self.assertEqual(self.c.pixel(5, 5), _WHITE) - - -class TestFilledShapes(unittest.TestCase): - def setUp(self): - self.c = make_fb(w=16, h=16) - self.c.fill(0) - - def test_fill(self): - self.assertEqual(fill(self.c, _WHITE), Area(0, 0, 16, 16)) - self.assertEqual(count_set(self.c), 16 * 16) - - def test_fill_rect(self): - self.assertEqual(fill_rect(self.c, 2, 2, 3, 3, _WHITE), Area(2, 2, 3, 3)) - self.assertEqual(count_set(self.c), 9) - - def test_rect_outline_vs_fill(self): - self.assertEqual(rect(self.c, 1, 1, 6, 6, _WHITE), Area(1, 1, 6, 6)) - outline = count_set(self.c) - self.c.fill(0) - rect(self.c, 1, 1, 6, 6, _WHITE, True) - filled = count_set(self.c) - self.assertGreater(filled, outline) - self.assertEqual(filled, 36) - - def test_round_rect_zero_radius_is_rect(self): - self.assertEqual(round_rect(self.c, 0, 0, 10, 8, 0, _WHITE), Area(0, 0, 10, 8)) - - -class TestCurves(unittest.TestCase): - def setUp(self): - self.c = make_fb(w=32, h=32) - self.c.fill(0) - - def test_circle_bbox_and_draws(self): - self.assertEqual(circle(self.c, 16, 16, 5, _WHITE), Area(11, 11, 10, 10)) - self.assertGreater(count_set(self.c), 0) - - def test_ellipse_returns_area(self): - self.assertIsInstance(ellipse(self.c, 16, 16, 6, 4, _WHITE), Area) - self.assertGreater(count_set(self.c), 0) - - def test_arc_returns_area(self): - self.assertIsInstance(arc(self.c, 16, 16, 6, 0, 90, _WHITE), Area) - - -class TestPolygons(unittest.TestCase): - def setUp(self): - self.c = make_fb(w=16, h=16) - self.c.fill(0) - - def test_triangle_outline_and_fill(self): - self.assertIsInstance(triangle(self.c, 0, 0, 8, 0, 4, 6, _WHITE), Area) - outline = count_set(self.c) - self.c.fill(0) - triangle(self.c, 0, 0, 8, 0, 4, 6, _WHITE, True) - self.assertGreater(count_set(self.c), outline) - - def test_poly_with_point_list(self): - bbox = poly(self.c, 0, 0, [(0, 0), (8, 0), (4, 6)], _WHITE) - self.assertEqual(bbox, Area(0, 0, 8, 6)) - self.assertGreater(count_set(self.c), 0) - - def test_poly_requires_three_vertices(self): - with self.assertRaises(ValueError): - poly(self.c, 0, 0, [(0, 0), (1, 1)], _WHITE) - - def test_poly_flat_array_must_be_even(self): - with self.assertRaises(ValueError): - poly(self.c, 0, 0, array("h", [0, 0, 1, 1, 2]), _WHITE) - - def test_polygon_requires_three_points(self): - with self.assertRaises(ValueError): - polygon(self.c, [(0, 0), (1, 1)], 0, 0, _WHITE) - - def test_polygon_returns_area(self): - self.assertEqual( - polygon(self.c, [(0, 0), (8, 0), (4, 6)], 0, 0, _WHITE), - Area(0, 0, 8, 6), - ) - - -class TestGradientAndBlit(unittest.TestCase): - def test_gradient_without_second_color_is_solid_fill(self): - c = make_fb(w=8, h=8) - c.fill(0) - self.assertEqual(gradient_rect(c, 0, 0, 8, 8, 0x1234), Area(0, 0, 8, 8)) - self.assertEqual(c.pixel(0, 0), 0x1234) - self.assertEqual(c.pixel(7, 7), 0x1234) - - def test_vertical_gradient_changes_down_the_rows(self): - c = make_fb(w=8, h=8) - c.fill(0) - gradient_rect(c, 0, 0, 8, 8, 0x0000, 0xFFFF, vertical=True) - self.assertEqual(c.pixel(0, 0), 0) - self.assertNotEqual(c.pixel(0, 7), 0) - - def test_blit_copies_pixels(self): - src = make_fb(w=4, h=4) - src.fill(0xABCD) - dst = make_fb(w=16, h=16) - dst.fill(0) - self.assertEqual(blit(dst, src, 2, 2), Area(2, 2, 4, 4)) - self.assertEqual(dst.pixel(2, 2), 0xABCD) - self.assertEqual(dst.pixel(0, 0), 0) - - def test_blit_fully_out_of_bounds_is_noop(self): - src = make_fb(w=4, h=4) - src.fill(0xABCD) - dst = make_fb(w=16, h=16) - dst.fill(0) - self.assertIsNone(blit(dst, src, 100, 100)) - self.assertEqual(count_set(dst), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_st7701.py b/tests/test_st7701.py deleted file mode 100644 index 141d07fd4..000000000 --- a/tests/test_st7701.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``st7701.run_init``.""" - -import unittest -from unittest import mock - -import _env # noqa: F401 - - -class _Pin: - def __init__(self): - self.values = [] - - def __call__(self, val=None): - if val is not None: - self.values.append(val) - return self.values[-1] if self.values else 0 - - -class TestST7701Init(unittest.TestCase): - def test_run_init_toggles_reset_and_cs(self): - from st7701 import LCDPins, run_init - - pins = LCDPins(pwr_en=_Pin(), cs=_Pin(), sda=_Pin(), clk=_Pin(), rst=_Pin()) - with mock.patch("st7701.sleep_ms"): - run_init(pins, init_sequence=[(0x11, b"\x00", 0)]) - self.assertIn(1, pins.rst.values) - self.assertIn(0, pins.rst.values) - self.assertTrue(any(v == 0 for v in pins.cs.values)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_standalone.py b/tests/test_standalone.py index 3718f9af5..8fc5296d0 100644 --- a/tests/test_standalone.py +++ b/tests/test_standalone.py @@ -21,7 +21,6 @@ _MULTIMER_SIBLINGS = ("displaysys", "eventsys", "graphics", "palettes") _EVENTSYS_SIBLINGS = ("displaysys", "graphics", "multimer", "palettes") -_GRAPHICS_SIBLINGS = ("displaysys", "eventsys", "multimer", "palettes") _DISPLAYSYS_SIBLINGS = ("eventsys", "graphics", "multimer", "palettes") _MULTIMER_CHILD = textwrap.dedent( @@ -97,37 +96,6 @@ """ ).format(siblings=list(_EVENTSYS_SIBLINGS)) -_GRAPHICS_CHILD = textwrap.dedent( - """ - import sys - - import graphics - from graphics import ( - Area, - Draw, - FrameBuffer, - Font, - RGB565, - circle, - rect, - text8, - ) - - forbidden = [m for m in {siblings!r} if m in sys.modules] - assert not forbidden, "graphics pulled in pydisplay modules: %r" % forbidden - - fb = FrameBuffer(bytearray(16 * 16 * 2), 16, 16, RGB565) - fb.fill(0) - assert fb.fill_rect(1, 1, 3, 3, 0xFFFF) == Area(1, 1, 3, 3) - assert fb.pixel(2, 2) == 0xFFFF - - draw = Draw(fb) - draw.circle(8, 8, 3, 0x1234) - text8(fb, "Hi", 0, 0, 0xFFFF) - - print("STANDALONE_OK") - """ -).format(siblings=list(_GRAPHICS_SIBLINGS)) _DISPLAYSYS_CHILD = textwrap.dedent( """ @@ -228,30 +196,6 @@ def test_eventsys_imports_and_runs_in_isolation(self): finally: shutil.rmtree(tmp, ignore_errors=True) - def test_graphics_imports_and_runs_in_isolation(self): - tmp = tempfile.mkdtemp(prefix="graphics_standalone_") - try: - shutil.copytree(_env.GRAPHICS_DIR, os.path.join(tmp, "graphics")) - - env = dict(os.environ) - env["PYTHONPATH"] = tmp - - proc = subprocess.run( - [sys.executable, "-c", _GRAPHICS_CHILD], - cwd=tmp, - env=env, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual( - proc.returncode, - 0, - msg=f"child failed\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}", - ) - self.assertIn("STANDALONE_OK", proc.stdout) - finally: - shutil.rmtree(tmp, ignore_errors=True) def test_displaysys_imports_and_runs_in_isolation(self): tmp = tempfile.mkdtemp(prefix="displaysys_standalone_") diff --git a/tests/test_stmpe610.py b/tests/test_stmpe610.py deleted file mode 100644 index e9d16af34..000000000 --- a/tests/test_stmpe610.py +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``stmpe610.STMPE610``.""" - -import sys -import types -import unittest -from unittest import mock - -import _env # noqa: F401 - - -class _FakePin: - OUT = 1 - IN = 0 - - def __init__(self, *_args, **_kwargs): - self._value = 1 - - def __call__(self, value=None): - if value is not None: - self._value = value - return self._value - - def init(self, *_args, **_kwargs): - return None - - -class TestSTMPE610(unittest.TestCase): - def _make_touch(self): - machine = types.ModuleType("machine") - machine.Pin = _FakePin - with mock.patch.dict(sys.modules, {"machine": machine}): - from stmpe610 import STMPE610 - - touch = STMPE610.__new__(STMPE610) - touch._width = 240 - touch._height = 320 - touch._rotation = 0 - touch._calib = ((0, 4095), (0, 4095)) - touch._write_byte = lambda *_args, **_kwargs: None - return touch - - def test_not_touched_returns_none(self): - touch = self._make_touch() - type(touch).touched = property(lambda self: False) - self.assertIsNone(touch.touch_point) - - def test_touch_point_uses_calibration(self): - touch = self._make_touch() - touch._calib = ((357, 3812), (390, 3555)) - type(touch).touched = property(lambda self: True) - type(touch).buffer_empty = property(lambda self: True) - touch._read_raw = lambda: (2048, 2048, 128) - point = touch.touch_point - self.assertIsNotNone(point) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_tt21100.py b/tests/test_tt21100.py deleted file mode 100644 index a357c77b8..000000000 --- a/tests/test_tt21100.py +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Brad Barnett -# -# SPDX-License-Identifier: MIT -"""Tests for ``tt21100.TT21100``.""" - -import struct -import sys -import types -import unittest -from unittest import mock - -import _env # noqa: F401 - - -def _patch_time(): - time_mod = types.ModuleType("time") - - def ticks_ms(): - return 0 - - def ticks_add(a, b): - return a + b - - def ticks_diff(a, b): - return a - b - - def sleep_ms(_ms): - return None - - time_mod.ticks_ms = ticks_ms - time_mod.ticks_add = ticks_add - time_mod.ticks_diff = ticks_diff - time_mod.sleep_ms = sleep_ms - return mock.patch.dict(sys.modules, {"time": time_mod}) - - -def _touch_packet(x, y, touch_id=0, pressure=32): - """Build a 17-byte TT21100 touch report (7-byte header + 10-byte point).""" - header = bytes([0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]) - point = struct.pack("xBHHBxxx", touch_id, x, y, pressure) - return header + point - - -class _FakeI2C: - def __init__(self, responses): - self._responses = list(responses) - self.calls = [] - - def readfrom(self, address, length, buf): - self.calls.append((address, length)) - if not self._responses: - raise AssertionError("unexpected readfrom") - data = self._responses.pop(0) - if isinstance(data, tuple): - data = bytes(data) - buf[:length] = data[:length] - - -class TestTT21100(unittest.TestCase): - def test_init_waits_for_clear(self): - i2c = _FakeI2C([bytes([1, 0]), bytes([0, 0])]) - with _patch_time(): - from tt21100 import TT21100 - - TT21100(i2c, address=0x24) - - def test_touches_empty_when_no_data(self): - i2c = _FakeI2C([bytes([0, 0]), bytes([0, 0])]) - with _patch_time(): - from tt21100 import TT21100 - - touch = TT21100(i2c) - self.assertEqual(touch.touches, []) - - def test_touches_header_only_packet(self): - i2c = _FakeI2C([bytes([0, 0]), bytes([7, 0]), b"\x00" * 7]) - with _patch_time(): - from tt21100 import TT21100 - - touch = TT21100(i2c) - self.assertEqual(touch.touches, []) - - def test_touches_parses_single_point(self): - packet = _touch_packet(120, 80, touch_id=3, pressure=40) - i2c = _FakeI2C( - [bytes([0, 0]), bytes([len(packet) & 0xFF, len(packet) >> 8]), packet] - ) - with _patch_time(): - from tt21100 import TT21100 - - touch = TT21100(i2c) - points = touch.touches - self.assertEqual(len(points), 1) - self.assertEqual(points[0]["x"], 120) - self.assertEqual(points[0]["y"], 80) - self.assertEqual(points[0]["id"], 3) - self.assertEqual(points[0]["pressure"], 40) - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/README.md b/tools/README.md index d23b24242..0d8254654 100644 --- a/tools/README.md +++ b/tools/README.md @@ -106,7 +106,7 @@ Installs `displaysys`, `usdl2`, `graphics-cmod`, and `lvgl-cpython` (no version | Script | Purpose | |--------|---------| -| [`test_testpypi_standalone.sh`](test_testpypi_standalone.sh) | Per-package TestPyPI venv import smoke (`multimer`, `displaysys`, `eventsys`, `pydisplay-graphics`; `--desktop` adds backend stacks) | +| [`test_testpypi_standalone.sh`](test_testpypi_standalone.sh) | Per-package TestPyPI venv import smoke (`multimer`, `displaysys`, `eventsys`, `graphics-py`; `--desktop` adds backend stacks) | ```bash ./tools/test_testpypi_standalone.sh diff --git a/tools/compare_graphics.py b/tools/compare_graphics.py index 352a85896..f11e1d847 100644 --- a/tools/compare_graphics.py +++ b/tools/compare_graphics.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 Brad Barnett # # SPDX-License-Identifier: MIT -"""Compare native ``graphics`` cmod to staged ``src/lib/graphics`` (``graphics_py``). +"""Compare native ``graphics`` cmod to staged ``../graphics/lib/graphics`` (``graphics_py``). Loads both implementations in one interpreter: ``import graphics`` (native cmod) and a copy of the pure-Python tree under ``.cursor/compare_graphics_py/graphics_py``. @@ -221,7 +221,7 @@ def _skip_unless_draw(rep, native, py, name): def stage_python_graphics(repo: str): - src = repo + "/src/lib/graphics" + src = repo + "/../graphics/lib/graphics" staging = repo + "/.cursor/compare_graphics_py" pkg_dir = staging + "/graphics_py" _makedirs(pkg_dir) @@ -268,8 +268,8 @@ def _check_exports(rep: _Reporter, native, py) -> None: rep.fail("python implementation(): {}".format(exc)) p_impl = None else: - if p_impl != "pydisplay_python": - rep.fail("python implementation: {!r} (expected 'pydisplay_python')".format(p_impl)) + if p_impl != "graphics_python": + rep.fail("python implementation: {!r} (expected 'graphics_python')".format(p_impl)) else: rep.ok("python implementation") @@ -776,7 +776,7 @@ def main(argv=None): if opts.get("help"): print( "Usage: compare_graphics_run.py [--repo PATH] [--quiet]\n" - "Compare native graphics cmod vs staged src/lib/graphics." + "Compare native graphics cmod vs staged ../graphics/lib/graphics." ) return 0 @@ -792,7 +792,7 @@ def main(argv=None): print() if result["status"] == "ok": print( - "All checks passed ({} ok, native graphics cmod vs src/lib/graphics).".format( + "All checks passed ({} ok, native graphics cmod vs ../graphics/lib/graphics).".format( result["checks_passed"] ) ) diff --git a/tools/compare_graphics_matrix.py b/tools/compare_graphics_matrix.py index d5ef1a4bc..12cae0beb 100644 --- a/tools/compare_graphics_matrix.py +++ b/tools/compare_graphics_matrix.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2026 Brad Barnett # # SPDX-License-Identifier: MIT -"""Cross-runtime matrix: native ``graphics`` cmod vs ``src/lib/graphics``. +"""Cross-runtime matrix: native ``graphics`` cmod vs ``../graphics/lib/graphics``. From repo root:: @@ -123,7 +123,7 @@ def ensure_graphics_cmod(python_exe: str, *, verbose: bool) -> tuple[bool, str]: impl = _graphics_impl(python_exe) if impl == "native_cmod": return True, "graphics-cmod already active" - if impl == "pydisplay_python": + if impl == "graphics_python": if verbose: print( "Installing graphics-cmod from TestPyPI for {}...".format(python_exe), diff --git a/tools/sibling_repos.py b/tools/sibling_repos.py index 821de58af..715caf86d 100644 --- a/tools/sibling_repos.py +++ b/tools/sibling_repos.py @@ -1,21 +1,32 @@ """ -Discover palettes / pdwidgets sibling repo ``src`` directories for the example harness. +Discover palettes / pdwidgets / graphics / usdl2 sibling repo ``lib`` directories +for the example harness. Search order per package: - 1. ``PYDISPLAY_PALETTES_SRC`` / ``PYDISPLAY_PDWIDGETS_SRC`` (optional override) - 2. ``/agent/repos//src`` - 3. ``~/gh/pydevices//src`` - 4. ``/..//src`` + 1. ``PYDISPLAY__LIB`` (optional override; ``PYDISPLAY_PALETTES_SRC`` etc. + still accepted as aliases) + 2. ``/agent/repos//lib`` + 3. ``~/gh/pydevices//lib`` + 4. ``/..//lib`` MicroPython-safe (no ``os.path`` / pathlib) so ``example_test_wrapper.py`` can import it. """ import os -_SIBLING_PACKAGES = ("palettes", "pdwidgets") +_SIBLING_PACKAGES = ("palettes", "pdwidgets", "graphics", "usdl2") _ENV_KEYS = { - "palettes": "PYDISPLAY_PALETTES_SRC", - "pdwidgets": "PYDISPLAY_PDWIDGETS_SRC", + "palettes": "PYDISPLAY_PALETTES_LIB", + "pdwidgets": "PYDISPLAY_PDWIDGETS_LIB", + "graphics": "PYDISPLAY_GRAPHICS_LIB", + "usdl2": "PYDISPLAY_USDL2_LIB", +} +# Backward-compatible aliases (older harness / docs used *_SRC). +_ENV_ALIASES = { + "palettes": ("PYDISPLAY_PALETTES_SRC",), + "pdwidgets": ("PYDISPLAY_PDWIDGETS_SRC",), + "graphics": ("PYDISPLAY_GRAPHICS_SRC",), + "usdl2": ("PYDISPLAY_USDL2_SRC",), } _PATHSEP = getattr(os, "pathsep", ":") @@ -115,20 +126,28 @@ def _repo_root(tools_dir=None): def _candidates(package, repo_root): return [ - _join("/agent/repos", package, "src"), - _expanduser(_join("~", "gh", "pydevices", package, "src")), - _normpath(_join(repo_root, "..", package, "src")), + _join("/agent/repos", package, "lib"), + _expanduser(_join("~", "gh", "pydevices", package, "lib")), + _normpath(_join(repo_root, "..", package, "lib")), ] def discover_sibling_src(package, repo_root=None, tools_dir=None): - """Return an existing sibling ``src`` path for *package*, or ``None``.""" + """Return an existing sibling ``lib`` path for *package*, or ``None``. + + Name kept for call-site compatibility; the directory is now ``lib/``. + """ env_key = _ENV_KEYS.get(package) if env_key: override = _env_get(env_key) if override: path = _normpath(_expanduser(override)) return path if _is_dir(path) else None + for alias in _ENV_ALIASES.get(package, ()): + override = _env_get(alias) + if override: + path = _normpath(_expanduser(override)) + return path if _is_dir(path) else None root = repo_root if repo_root is not None else _repo_root(tools_dir) for candidate in _candidates(package, root): @@ -139,7 +158,7 @@ def discover_sibling_src(package, repo_root=None, tools_dir=None): def discover_sibling_srcs(repo_root=None, tools_dir=None): - """Return existing sibling ``src`` paths in package order.""" + """Return existing sibling ``lib`` paths in package order.""" found = [] for package in _SIBLING_PACKAGES: path = discover_sibling_src(package, repo_root=repo_root, tools_dir=tools_dir) @@ -170,7 +189,7 @@ def apply_sibling_env(env, repo_root=None, tools_dir=None, prepend_paths=None): def prepend_sibling_sys_path(repo_root=None, tools_dir=None): - """Insert discovered sibling ``src`` dirs at the front of ``sys.path``.""" + """Insert discovered sibling ``lib`` dirs at the front of ``sys.path``.""" import sys added = [] diff --git a/tools/test_testpypi_standalone.sh b/tools/test_testpypi_standalone.sh index 90306f2b8..e15e991a1 100755 --- a/tools/test_testpypi_standalone.sh +++ b/tools/test_testpypi_standalone.sh @@ -91,7 +91,7 @@ r.start_timer() print('eventsys', type(r).__name__) " -test_package pydisplay-graphics "import graphics; print('graphics', graphics.implementation())" +test_package graphics-py "import graphics; print('graphics', graphics.implementation())" if [[ "$DESKTOP" -eq 1 ]]; then # usdl2 / pygame-ce are runtime deps, not pip requires of the displaysys wheel. diff --git a/web/pyscript/micropython.toml b/web/pyscript/micropython.toml index cdcb580a5..102ed5604 100644 --- a/web/pyscript/micropython.toml +++ b/web/pyscript/micropython.toml @@ -10,7 +10,6 @@ interpreter = "./vendor/micropython/micropython.mjs" "./src/add_ons/display_driver.py" = "/add_ons/" "./src/add_ons/displaybuf.py" = "/add_ons/" "./src/add_ons/fetch_ph_gui.py" = "/add_ons/" -"./src/add_ons/framebuf.py" = "/add_ons/" "./src/add_ons/hardware_setup.py" = "/add_ons/" "./src/add_ons/itertools.py" = "/add_ons/" "./src/add_ons/keypins.py" = "/add_ons/" @@ -24,7 +23,6 @@ interpreter = "./vendor/micropython/micropython.mjs" "./src/add_ons/tft_write.py" = "/add_ons/" "./src/add_ons/touch_setup.py" = "/add_ons/" "./src/add_ons/uctypes.py" = "/add_ons/" -"./src/add_ons/usdl2.py" = "/add_ons/" "./src/add_ons/viper_tools.py" = "/add_ons/" "./src/add_ons/wifi.py" = "/add_ons/" "./src/add_ons/fonts/__init__.py" = "/add_ons/fonts/" @@ -68,20 +66,6 @@ interpreter = "./vendor/micropython/micropython.mjs" "./src/lib/eventsys/keys.py" = "/lib/eventsys/" "./src/lib/eventsys/touch_keypad.py" = "/lib/eventsys/" -"./src/lib/graphics/__init__.py" = "/lib/graphics/" -"./src/lib/graphics/_area.py" = "/lib/graphics/" -"./src/lib/graphics/_blit_hooks.py" = "/lib/graphics/" -"./src/lib/graphics/_bmp565.py" = "/lib/graphics/" -"./src/lib/graphics/_clip.py" = "/lib/graphics/" -"./src/lib/graphics/_draw.py" = "/lib/graphics/" -"./src/lib/graphics/_files.py" = "/lib/graphics/" -"./src/lib/graphics/_font.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x14.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x16.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x8.py" = "/lib/graphics/" -"./src/lib/graphics/_framebuf_plus.py" = "/lib/graphics/" -"./src/lib/graphics/_shapes.py" = "/lib/graphics/" - "./src/lib/multimer/__init__.py" = "/lib/multimer/" "./src/lib/multimer/_async_timer.py" = "/lib/multimer/" "./src/lib/multimer/_asyncio_loader.py" = "/lib/multimer/" @@ -98,4 +82,19 @@ interpreter = "./vendor/micropython/micropython.mjs" "./src/lib/multimer/_backends/threading.py" = "/lib/multimer/_backends/" "./src/lib/multimer/_backends/win32.py" = "/lib/multimer/_backends/" + +"../graphics/lib/graphics/__init__.py" = "/lib/graphics/" +"../graphics/lib/graphics/_area.py" = "/lib/graphics/" +"../graphics/lib/graphics/_blit_hooks.py" = "/lib/graphics/" +"../graphics/lib/graphics/_bmp565.py" = "/lib/graphics/" +"../graphics/lib/graphics/_clip.py" = "/lib/graphics/" +"../graphics/lib/graphics/_draw.py" = "/lib/graphics/" +"../graphics/lib/graphics/_files.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x14.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x16.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x8.py" = "/lib/graphics/" +"../graphics/lib/graphics/_framebuf_plus.py" = "/lib/graphics/" +"../graphics/lib/graphics/_shapes.py" = "/lib/graphics/" +"../graphics/lib/graphics/framebuf.py" = "/lib/graphics/" "./src/add_ons/ps_loader.py" = "/" diff --git a/web/pyscript/pyodide.toml b/web/pyscript/pyodide.toml index 54b6ac435..b0652a580 100644 --- a/web/pyscript/pyodide.toml +++ b/web/pyscript/pyodide.toml @@ -10,7 +10,6 @@ interpreter = "./vendor/pyodide/pyodide.mjs" "./src/add_ons/display_driver.py" = "/add_ons/" "./src/add_ons/displaybuf.py" = "/add_ons/" "./src/add_ons/fetch_ph_gui.py" = "/add_ons/" -"./src/add_ons/framebuf.py" = "/add_ons/" "./src/add_ons/hardware_setup.py" = "/add_ons/" "./src/add_ons/itertools.py" = "/add_ons/" "./src/add_ons/keypins.py" = "/add_ons/" @@ -24,7 +23,6 @@ interpreter = "./vendor/pyodide/pyodide.mjs" "./src/add_ons/tft_write.py" = "/add_ons/" "./src/add_ons/touch_setup.py" = "/add_ons/" "./src/add_ons/uctypes.py" = "/add_ons/" -"./src/add_ons/usdl2.py" = "/add_ons/" "./src/add_ons/viper_tools.py" = "/add_ons/" "./src/add_ons/wifi.py" = "/add_ons/" "./src/add_ons/fonts/__init__.py" = "/add_ons/fonts/" @@ -68,20 +66,6 @@ interpreter = "./vendor/pyodide/pyodide.mjs" "./src/lib/eventsys/keys.py" = "/lib/eventsys/" "./src/lib/eventsys/touch_keypad.py" = "/lib/eventsys/" -"./src/lib/graphics/__init__.py" = "/lib/graphics/" -"./src/lib/graphics/_area.py" = "/lib/graphics/" -"./src/lib/graphics/_blit_hooks.py" = "/lib/graphics/" -"./src/lib/graphics/_bmp565.py" = "/lib/graphics/" -"./src/lib/graphics/_clip.py" = "/lib/graphics/" -"./src/lib/graphics/_draw.py" = "/lib/graphics/" -"./src/lib/graphics/_files.py" = "/lib/graphics/" -"./src/lib/graphics/_font.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x14.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x16.py" = "/lib/graphics/" -"./src/lib/graphics/_font_8x8.py" = "/lib/graphics/" -"./src/lib/graphics/_framebuf_plus.py" = "/lib/graphics/" -"./src/lib/graphics/_shapes.py" = "/lib/graphics/" - "./src/lib/multimer/__init__.py" = "/lib/multimer/" "./src/lib/multimer/_async_timer.py" = "/lib/multimer/" "./src/lib/multimer/_asyncio_loader.py" = "/lib/multimer/" @@ -98,4 +82,19 @@ interpreter = "./vendor/pyodide/pyodide.mjs" "./src/lib/multimer/_backends/threading.py" = "/lib/multimer/_backends/" "./src/lib/multimer/_backends/win32.py" = "/lib/multimer/_backends/" + +"../graphics/lib/graphics/__init__.py" = "/lib/graphics/" +"../graphics/lib/graphics/_area.py" = "/lib/graphics/" +"../graphics/lib/graphics/_blit_hooks.py" = "/lib/graphics/" +"../graphics/lib/graphics/_bmp565.py" = "/lib/graphics/" +"../graphics/lib/graphics/_clip.py" = "/lib/graphics/" +"../graphics/lib/graphics/_draw.py" = "/lib/graphics/" +"../graphics/lib/graphics/_files.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x14.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x16.py" = "/lib/graphics/" +"../graphics/lib/graphics/_font_8x8.py" = "/lib/graphics/" +"../graphics/lib/graphics/_framebuf_plus.py" = "/lib/graphics/" +"../graphics/lib/graphics/_shapes.py" = "/lib/graphics/" +"../graphics/lib/graphics/framebuf.py" = "/lib/graphics/" "./src/add_ons/ps_loader.py" = "/"