diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index f31cf33..5393f99 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -20,7 +20,7 @@ jobs: - name: Build HTML documentation with Sphinx run: | make html - make html SPHINXOPTS="-W --keep-going -n" + make html SPHINXOPTS="-W --keep-going" working-directory: docs - uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/check_docs.yaml b/.github/workflows/check_docs.yaml index 54c1d14..354228e 100644 --- a/.github/workflows/check_docs.yaml +++ b/.github/workflows/check_docs.yaml @@ -18,7 +18,7 @@ jobs: - name: Build HTML documentation with Sphinx run: | make html - make html SPHINXOPTS="-W --keep-going -n" + make html SPHINXOPTS="-W --keep-going" working-directory: docs - uses: actions/upload-artifact@v4 with: diff --git a/docs/source/conf.py b/docs/source/conf.py index 559d460..fd3616f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,6 +17,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', 'autoapi.extension' ] @@ -26,7 +27,8 @@ # -- Options for intersphinx ------------------------------------------------- intersphinx_mapping = { - 'micropython': ('https://docs.micropython.org/en/latest', None) + 'micropython': ('https://docs.micropython.org/en/latest', None), + 'python': ('https://docs.python.org/3', None) } # -- Options for HTML output ------------------------------------------------- @@ -75,4 +77,17 @@ # -- Options for autoapi ----------------------------------------------------- autoapi_dirs = ['../../src'] -autoapi_root = 'api' \ No newline at end of file +autoapi_root = 'api' +autoapi_file_patterns = ['*.pyi', '*.py'] +autoapi_options = [ + 'members', + 'undoc-members', + 'private-members', + 'show-inheritance', + 'show-module-summary', + 'imported-members', +] +autoapi_ignore = ["*_data_view_math*"] + +add_module_names = False +python_maximum_signature_line_length = 60 \ No newline at end of file diff --git a/docs/source/user_guide/tutorial.rst b/docs/source/user_guide/tutorial.rst index d1ac59b..53add81 100644 --- a/docs/source/user_guide/tutorial.rst +++ b/docs/source/user_guide/tutorial.rst @@ -325,8 +325,6 @@ like the following: .. image:: hello_font.png :width: 160 -TODO: support alright fonts - Markers and Scatterplots ------------------------ @@ -351,6 +349,20 @@ key-color for transparency. The |ColoredBitmaps| shapes render 1-bit framebuffers in the specified colors. +Convenience Methods +=================== + +So far we have been using a pattern of two-step creation of shapes, where +we first create a shape and then add it to a layer of a surface. Since you +almost always want to add shapes to a layer immediatel after creating them, +the |Surface| class has a collection of methods for creating and adding +standard shapes in one step. + +TODO: example + + + +.. |FrameBuffer| replace:: :py:class:`~framebuf.FrameBuffer` .. |Surface| replace:: :py:class:`~tempe.surface.Surface` .. |refresh| replace:: :py:meth:`~tempe.surface.Surface.refresh` .. |Shape| replace:: :py:class:`~tempe.shapes.Shape` diff --git a/src/tempe/data_view_math.py b/src/tempe/_data_view_math.py similarity index 100% rename from src/tempe/data_view_math.py rename to src/tempe/_data_view_math.py diff --git a/src/tempe/_data_view_math.pyi b/src/tempe/_data_view_math.pyi new file mode 100644 index 0000000..4b09cf3 --- /dev/null +++ b/src/tempe/_data_view_math.pyi @@ -0,0 +1,3 @@ +"""Internal implementation for numeric operations on DataViews.""" + +__all__ = [] \ No newline at end of file diff --git a/src/tempe/bitmaps.pyi b/src/tempe/bitmaps.pyi new file mode 100644 index 0000000..3540467 --- /dev/null +++ b/src/tempe/bitmaps.pyi @@ -0,0 +1,53 @@ + +from array import array +from collections.abc import Sequence, Iterable +import framebuf +from typing import Any + +from .shapes import ColoredGeometry, Shape, BLIT_KEY_RGB565, point, rectangle + + +class Bitmaps(Shape): + """Draw framebuffer bitmaps at points""" + + def __init__( + self, + geometry: Iterable[point], + buffers: Iterable[framebuf.FrameBuffer], + *, + key: int | None = None, + palette: Sequence[int] | None = None, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def update( + self, + geometry: Iterable[point] | None = None, + buffers: Iterable[framebuf.FrameBuffer] | None = None, + **kwargs: Any, + ): ... + + def __iter__(self) -> tuple[point, framebuf.FrameBuffer]: ... + + +class ColoredBitmaps(ColoredGeometry[point]): + """Draw 1-bit framebuffers bitmaps at points in given colors.""" + + def __init__( + self, + geometry: Iterable[point], + colors: Iterable[int], + buffers: Iterable[framebuf.FrameBuffer], + *, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def update( + self, + geometry: Iterable[point] | None = None, + colors: Iterable[int] | None = None, + buffers: Iterable[framebuf.FrameBuffer] | None = None, + **kwargs: Any, + ): ... diff --git a/src/tempe/data_view.py b/src/tempe/data_view.py index 4d17acf..f2fabab 100644 --- a/src/tempe/data_view.py +++ b/src/tempe/data_view.py @@ -29,7 +29,7 @@ def __getitem__(self, index): return self.data[index] def __add__(self, other): - from .data_view_math import Add + from ._data_view_math import Add try: iter(other) @@ -38,7 +38,7 @@ def __add__(self, other): return Add(self, Repeat(other)) def __radd__(self, other): - from .data_view_math import Add + from ._data_view_math import Add try: iter(other) @@ -47,7 +47,7 @@ def __radd__(self, other): return Add(Repeat(other), self) def __sub__(self, other): - from .data_view_math import Subtract + from ._data_view_math import Subtract try: iter(other) @@ -56,7 +56,7 @@ def __sub__(self, other): return Subtract(self, Repeat(other)) def __rsub__(self, other): - from .data_view_math import Subtract + from ._data_view_math import Subtract try: iter(other) @@ -65,7 +65,7 @@ def __rsub__(self, other): return Subtract(Repeat(other), self) def __mul__(self, other): - from .data_view_math import Multiply + from ._data_view_math import Multiply try: iter(other) @@ -74,7 +74,7 @@ def __mul__(self, other): return Multiply(self, Repeat(other)) def __rmul__(self, other): - from .data_view_math import Multiply + from ._data_view_math import Multiply try: iter(other) @@ -83,7 +83,7 @@ def __rmul__(self, other): return Multiply(Repeat(other), self) def __floordiv__(self, other): - from .data_view_math import FloorDivide + from ._data_view_math import FloorDivide try: iter(other) @@ -92,7 +92,7 @@ def __floordiv__(self, other): return FloorDivide(self, Repeat(other)) def __rfloordiv__(self, other): - from .data_view_math import FloorDivide + from ._data_view_math import FloorDivide try: iter(other) @@ -101,7 +101,7 @@ def __rfloordiv__(self, other): return FloorDivide(Repeat(other), self) def __truediv__(self, other): - from .data_view_math import Divide + from ._data_view_math import Divide try: iter(other) @@ -110,7 +110,7 @@ def __truediv__(self, other): return Divide(self, Repeat(other)) def __rtruediv__(self, other): - from .data_view_math import Divide + from ._data_view_math import Divide try: iter(other) @@ -119,22 +119,22 @@ def __rtruediv__(self, other): return Divide(Repeat(other), self) def __neg__(self): - from .data_view_math import Neg + from ._data_view_math import Neg return Neg(self) def __pos__(self): - from .data_view_math import Pos + from ._data_view_math import Pos return Pos(self) def __abs__(self): - from .data_view_math import Abs + from ._data_view_math import Abs return Abs(self) def __invert__(self): - from .data_view_math import Invert + from ._data_view_math import Invert return Invert(self) @@ -259,8 +259,8 @@ class Count(DataView): """DataView that counts indefinitely from a start by a step.""" def __init__(self, start=0, step=1): - self.start = 0 - self.step = 1 + self.start = start + self.step = step def __iter__(self): i = self.start @@ -342,5 +342,5 @@ def __init__(self, data, n): self.n = n def __iter__(self): - for index in range(n): - yield self.data[index * (len(self.data) - 1) // (n - 1)] + for index in range(self.n): + yield self.data[index * (len(self.data) - 1) // (self.n - 1)] diff --git a/src/tempe/data_view.pyi b/src/tempe/data_view.pyi new file mode 100644 index 0000000..0d0f6af --- /dev/null +++ b/src/tempe/data_view.pyi @@ -0,0 +1,125 @@ +"""The DataView class and its subclasses. + +The following informal types are used in this module: + +.. py:type:: DataType + + This is the underlying data-type of the data being viewed; it is the + type that will be produced when the DataView is iterated. +""" + +from collections.abc import Sequence, Iterator, Iterable +from typing import Any, Generic, TypeVar + + +DataType = TypeVar("DataType") + + +class DataView(Generic[DataType]): + """The base dataview class""" + + @classmethod + def create(cls, data: Any) -> DataView: + """Create an appropriate dataclass instance from an object.""" + + def __init__(self, data: Iterable[DataType]): ... + + def __len__(self) -> int | None: ... + + def __iter__(self) -> Iterator[DataType]: ... + + def __getitem__(self, index: int | slice) -> DataType | DataView[DataType]: ... + + def __add__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __radd__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __sub__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __rsub__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __mul__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __rmul__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __floordiv__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __rfloordiv__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __truediv__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __rtruediv__(self, other: DataType | Iterable[DataType]) -> DataView[DataType]: ... + + def __neg__(self) -> DataView[DataType]: ... + + def __pos__(self) -> DataView[DataType]: ... + + def __abs__(self) -> DataView[DataType]: ... + + def __invert__(self) -> DataView[DataType]: ... + + +class Cycle(DataView[DataType]): + """A Dataview which extends an iterable by repeating cyclically.""" + + def __len__(self) -> None: ... + + +class ReflectedCycle(DataView[DataType]): + """A Dataview which extends an iterable by repeating in reverse.""" + + def __len__(self) -> None: ... + + +class RepeatLast(DataView[DataType]): + """A Dataview which extends an iterable by repeating the last value.""" + + def __len__(self) -> None: ... + + +class Repeat(DataView[DataType]): + """A Dataview broadcasts a scalar as an infinitely repeating value.""" + + def __init__(self, data: DataType): ... + + def __len__(self) -> None: ... + + +class Count(DataView[int]): + """DataView that counts indefinitely from a start by a step.""" + + def __init__(self, start: int = 0, step: int = 1): ... + + def __len__(self) -> None: ... + + +class Range(DataView[int]): + """DataView that efficiently represents a range.""" + + def __init__(self, start: int, stop: int | None = None, step: int = 1): ... + + def __len__(self) -> int: ... + + +class Slice(DataView[DataType]): + """Take a slice of another DataView""" + + def __init__(self, data: Iterable[DataType], start: int, stop: int | None = None, step: int = 1): ... + + +class Interpolated(DataView[DataType]): + """Produce n evenly spaced elements from the data source.""" + + def __init__(self, data: Sequence[DataType], n: int): ... + +__all__ = [ + "DataView", + "Cycle", + "ReflectedCycle", + "RepeatLast", + "Repeat", + "Count", + "Range", + "Slice", + "Interpolated", +] \ No newline at end of file diff --git a/src/tempe/display.py b/src/tempe/display.py index ad43fe4..6428c69 100644 --- a/src/tempe/display.py +++ b/src/tempe/display.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024-present Unital Software +# +# SPDX-License-Identifier: MIT class Display: diff --git a/src/tempe/display.pyi b/src/tempe/display.pyi new file mode 100644 index 0000000..9aaa853 --- /dev/null +++ b/src/tempe/display.pyi @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2024-present Unital Software +# +# SPDX-License-Identifier: MIT + +"""Base Display class and FileDisplay class. + +This module defines the Display abstract base class as well as a concrete +implementation that renders to a binary file. + +Users of specific hardware may need to define their own subclasses that +can send updates to the underlying device. +""" + +from array import array +from typing import Self + + +class Display: + """Abstract base class for Displays""" + + def blit(self, buffer: array, x: int, y: int, w: int, h: int): + """Render the buffer to the given rectangle of the Display. + + The array buffer must match the width and height, and the edges + of the bounding rectangle should lie within the Display. + + Concrete subclasses must implement this method, either to + directly render partial updates to the underlying device, or to + render to a complete framebuffer which is then rendered. + """ + raise NotImplementedError + + def clear(self) -> None: + """Clear the display, setting all pixels to 0.""" + raise NotImplementedError + + +class FileDisplay(Display): + """Display that renders raw RGB565 data to a file. + + FileDisplay can be used as a context manager to open and close the + underlying file object automatically. + """ + + def __init__(self, name: str, size: tuple[int, int] = (320, 240)): ... + + def __enter__(self) -> Self: ... + + def __exit__(self, *args) -> bool: ... + diff --git a/src/tempe/font.py b/src/tempe/font.py index 7b7728f..4140398 100644 --- a/src/tempe/font.py +++ b/src/tempe/font.py @@ -1,4 +1,5 @@ - +from array import array +import framebuf class AbstractFont: diff --git a/src/tempe/geometry.pyi b/src/tempe/geometry.pyi new file mode 100644 index 0000000..d3f6c07 --- /dev/null +++ b/src/tempe/geometry.pyi @@ -0,0 +1,37 @@ + +from array import array + +from .data_view import DataView + +from collections.abc import Sequence, Iterator, Iterable +from typing import Any, Generic, TypeVar + + +_T = TypeVar("_T", bound=Sequence[int]) + + +class Geometry(DataView[_T]): + """Efficient storage of geometric information.""" + + +class RowGeometry(Geometry): + """Geometry where coordinates are provided as ragged rows.""" + + @classmethod + def from_lists(cls, rows: Sequence[Sequence[int]]) -> RowGeometry: ... + + +class ColumnGeometry(Geometry): + """Geometry where coordinates are provided as ragged columns""" + + +class StripGeometry(Geometry): + """Geometry generating connected strip of n-gons from vertices. + + Iterator provides vertex point buffers of the form [x0, y0, x1, y1, ...]. + """ + + #: Grabbing pairs of coordinates for vertices. + n_coords: int = 2 + + def __init__(self, geometry, n_vertices: int = 2, step: int = 1): ... diff --git a/src/tempe/markers.pyi b/src/tempe/markers.pyi new file mode 100644 index 0000000..ea350fb --- /dev/null +++ b/src/tempe/markers.pyi @@ -0,0 +1,67 @@ +from collections.abc import Iterable +from array import array +import framebuf +from typing import Any + +from .geometry import Geometry +from .shapes import ColoredGeometry, BLIT_KEY_RGB565, rectangle, point_length +from .surface import Surface + + +class Marker: + """Enum for marker types""" + PIXEL = 0 + CIRCLE = 1 + SQUARE = 2 + HLINE = 3 + VLINE = 4 + PLUS = 5 + CROSS = 6 + + +class Markers(ColoredGeometry[point_length]): + """Display sized, colored markers at points. + + Parameters + ---------- + geometry : Iterable[geom] | None + The sequence of geometries to render. + colors : Iterable[int] | None + The sequence of colors for each geometry. + markers : Iterable[Any] | None + The sequence of colors for each geometry. + surface : Surface | None + The surface which this shape is associated with. + clip : rectangle | None + An (x, y, w, h) tuple to clip drawing to - anything drawn outside + this region will not show on the display. + """ + + def __init__( + self, + geometry: Geometry[point_length], + colors: Iterable[int], + markers: Iterable[Any], + *, + surface: Surface | None = None, + clip: rectangle | None = None, + ): ... + + def update( + self, + geometry: Iterable[point_length] | None = None, + colors: Iterable[int] | None = None, + markers: Iterable[Any] | None = None, + **kwargs: Any, + ): + """Update the state of the Shape, marking a redraw as needed. + + Parameters + ---------- + geometry : Iterable[geom] | None + The sequence of geometries to render. + colors : Iterable[int] | None + The sequence of colors for each geometry. + markers : Iterable[Any] | None + The sequence of colors for each geometry. + """ diff --git a/src/tempe/raster.pyi b/src/tempe/raster.pyi new file mode 100644 index 0000000..9e1aac4 --- /dev/null +++ b/src/tempe/raster.pyi @@ -0,0 +1,51 @@ +"""This module defined the Raster class. + +This is an internal class which handles the logic of providing a +framebuffer for a rectangular region of a surface, as well as clipping +operations. +""" + +from array import array +import framebuf + +from typing import Self + +class Raster: + """A rectangular buffer that can be drawn on by a surface. + + This class is used internally, so most end-users do not need to create + them. + + Attributes + ---------- + fbuf : framebuf.FrameBuffer + The framebuffer built on the buffer. + x : int + The x-offset of the framebuffer relative to the Surface. + y : int + The y-offset of the framebuffer relative to the Surface. + """ + + def __init__( + self, + buf: array, + x: int, + y: int, + w: int, + h: int, + stride: int | None = None, + offset: int = 0, + ): ... + + @classmethod + def from_rect(cls, x: int, y: int, w: int, h: int) -> Self: + """Create a Raster with a new buffer for the given rectangle.""" + + def clip(self, x: int, y: int, w: int, h: int) -> Self | None: + """Create a Raster sharing the same buffer, clipped to the rectangle. + + If there is no intersection between the raster and the rectangle, then + None is returned. + """ + +__all__ = ["Raster"] diff --git a/src/tempe/shapes.py b/src/tempe/shapes.py index 6ac7e88..59f9c44 100644 --- a/src/tempe/shapes.py +++ b/src/tempe/shapes.py @@ -3,7 +3,7 @@ import asyncio #: Transparent color when blitting bitmaps. -BLIT_KEY_RGB565 = 0b0000000000100000 +BLIT_KEY_RGB565 = const(0b0000000000100000) class Shape: @@ -11,7 +11,6 @@ class Shape: def __init__(self, surface=None, clip=None): self.surface = surface - self.needs_redraw = asyncio.Event() self.clip = clip def draw(self, buffer, x=0, y=0): @@ -26,6 +25,7 @@ def update(self): def _bounds(self): raise NotImplementedError() + class ColoredGeometry(Shape): """ABC for geometries with colors applied.""" diff --git a/src/tempe/shapes.pyi b/src/tempe/shapes.pyi new file mode 100644 index 0000000..5b5ee5e --- /dev/null +++ b/src/tempe/shapes.pyi @@ -0,0 +1,341 @@ +"""Shape classes which efficiently draw fundamental geometries. + +This module provides the core shape abstract base classes, along with +concrete classes that implement drawing of multiple rectangles, lines, +polygons and ellipses. The latter are wrappers around calls to the +corresponding |FrameBuffer| routines, and have the same constraints, +such as lines being limited to 1 pixel width. + +The shape types have expectations about the types of the data produced +by the geometries. These are not formally defined as types or classes +for speed and memory efficiency, but are treated as type aliases by +Python type annotations. + +.. py:type:: geom + + A generic geometry as a sequence of ints of unspecified length. + +.. py:type:: point + + A sequence of ints of the form (x, y). + +.. py:type:: points + + A sequence of ints of the form (x0, y0, x1, y1). + +.. py:type:: point_length + + A sequence of ints of the form (x, y, length). The length can also be + used as a size parameter for markers or radius of a circle, as appropriate. + +.. py:type:: point_array + + An array of signed 16-bit integers giving point coordinates of the + form ``array('h', [x0, y0, x1, y1, ...])``. + +.. py:type:: rectangle + + A sequence of ints of the form (x, y, w, h). + +.. py:type:: ellipse + + A sequence of ints of the form (center_x, center_y, radius_x, radius_y). + +.. |FrameBuffer| replace:: :py:class:`~framebuf.FrameBuffer` + +Attributes +---------- + +BLIT_KEY_RGB565 : int + The default transparency color when blitting an RGB565 |FrameBuffer|. + This is a color which has 1 in the 6th (least significant) bit of + green, and 0 red and blue. +""" + +from array import array +import asyncio +from collections.abc import Sequence, Iterable +from framebuf import FrameBuffer +from typing import Any, Generic, TypeVar, TypeAlias + +from .geometry import Geometry +from .data_view import DataView + +geom = TypeVar("geom", bound=Sequence[int]) +point: TypeAlias = tuple[int, int] +points: TypeAlias = tuple[int, int, int, int] +point_array: TypeAlias = array[int] +point_length: TypeAlias = tuple[int, int, int] +rectangle: TypeAlias = tuple[int, int, int, int] +ellipse: TypeAlias = tuple[int, int, int, int] + + +#: Transparent color when blitting bitmaps. +BLIT_KEY_RGB565 = 0b0000000000100000 + + +class Shape: + """ABC for drawable objects. + + Parameters + ---------- + surface : Surface | None + The surface which this shape is associated with. + clip : rectangle | None + An (x, y, w, h) tuple to clip drawing to - anything drawn outside + this region will not show on the display. + """ + + def __init__( + self, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def draw(self, buffer: FrameBuffer, x: int = 0, y: int = 0) -> None: + """Render the shape into the given buffer offset by x and y. + + Concrete subclasses need to translate any drawing they do in the + buffer by the x and y offsets. + + Parameters + ---------- + buffer : framebuf.FrameBuffer + An array of pixels to be rendered into. + x : int + The x-offset of the left side of the buffer on the + surface. + y : int + The y-offset of the top side of the buffer on the + surface. + """ + + def update(self, **kwargs: Any) -> None: + """Update the state of the Shape, marking a redraw as needed. + + This adds the clipping rectangle to the damaged regions of the + Surface. Subclasses should allow additional keyword arguments + for updating geometry and other state as needed, and should + either call ``super()`` or otherwise ensure that the Surface + has its damage region updated. + """ + + def _bounds(self) -> rectangle: + """Compute the bounds of the Shape. + + Subclasses need to override this. + """ + + +class ColoredGeometry(Shape, Generic[geom]): + """ABC for geometries with colors applied. + + These classes draw each shape from the Geometry using the + corresponding color from the color data. ColoredGeometries + are iterable with the iterator producing (geometry, color) + pairs. + + Parameters + ---------- + geometry : Iterable[geom] + The sequence of geometries to render. + colors : Iterable[int] + The sequence of colors for each geometry. + surface : Surface | None + The surface which this shape is associated with. + clip : rectangle | None + An (x, y, w, h) tuple to clip drawing to - anything drawn outside + this region will not show on the display. + """ + + def __init__( + self, + geometry: Iterable[geom], + colors: Iterable[int], + *, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def update( + self, + geometry: Iterable[geom] | None = None, + colors: Iterable[int] | None = None, + **kwargs: DataView[Any] | None, + ): + """Update the state of the Shape, marking a redraw as needed. + + Parameters + ---------- + geometry : Geometry[geom] | None + The sequence of geometries to render. + colors : Iterable[int] | None + The sequence of colors for each geometry. + """ + + def __iter__(self) -> tuple: ... + + +class FillableGeometry(ColoredGeometry[geom]): + """ABC for geometries which can either be filled or stroked. + + Stroked outlines always have line with 1. + + Parameters + ---------- + geometry : Iterable[geom] + The sequence of geometries to render. + colors : Iterable[int] + The sequence of colors for each geometry. + fill : bool + Whether to fill the shape or to draw the outline. + surface : Surface | None + The surface which this shape is associated with. + clip : rectangle | None + An (x, y, w, h) tuple to clip drawing to - anything drawn outside + this region will not show on the display. + """ + + def __init__( + self, + geometry: Iterable[geom], + colors: Iterable[int], + *, + fill: bool = True, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + +class Lines(ColoredGeometry[points]): + """Render multiple colored line segments with line-width 1. + + Geometry should produce x0, y0, x1, y1 arrays. + """ + + def __init__( + self, + geometry: Iterable[points], + colors: Iterable[int], + *, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[points, int]: ... + + +class HLines(ColoredGeometry[point_length]): + """Render multiple colored horizontal line segments with line-width 1. + + Geometry should produce x0, y0, length arrays. + """ + + def __init__( + self, + geometry: Iterable[point_length], + colors: Iterable[int], + *, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[point_length, int]: ... + + +class VLines(ColoredGeometry[point_length]): + """Render multiple colored vertical line segments with line-width 1. + + Geometry should produce x0, y0, length arrays. + """ + + def __init__( + self, + geometry: Iterable[point_length], + colors: Iterable[int], + *, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[point_length, int]: ... + + +class Polygons(FillableGeometry[point_array]): + """Render multiple polygons. + + Geometry should produce vertex arrays of the form [x0, y0, x1, y1, ...]. + """ + + def __iter__(self) -> tuple[point_array, int]: ... + + +class Rectangles(FillableGeometry[rectangle]): + """Render multiple rectangles. + + Geometry should produce x, y, w, h arrays. + """ + + def __init__( + self, + geometry: Iterable[rectangle], + colors: Iterable[int], + *, + fill: bool = True, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[rectangle, int]: ... + + +class Circles(FillableGeometry[point_length]): + """Render multiple circles. + + Geometry should produce cx, cy, r arrays. + """ + + def __init__( + self, + geometry: Iterable[point_length], + colors: Iterable[int], + *, + fill: bool = True, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[point_length, int]: ... + + +class Ellipses(FillableGeometry[ellipse]): + """Render multiple ellipses. + + Geometry should produce cx, cy, rx, ry arrays. + """ + + def __init__( + self, + geometry: Iterable[ellipse], + colors: Iterable[int], + *, + fill: bool = True, + surface: "tempe.surface.Surface | None" = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[ellipse, int]: ... + + +__all__ = [ + "Shape", + "ColoredGeometry", + "FillableGeometry", + "Lines", + "HLines", + "VLines", + "Polygons", + "Rectangles", + "Circles", + "Ellipses", +] \ No newline at end of file diff --git a/src/tempe/surface.py b/src/tempe/surface.py index d5d4219..139415a 100644 --- a/src/tempe/surface.py +++ b/src/tempe/surface.py @@ -3,12 +3,10 @@ from .raster import Raster from .shapes import Polygons, Rectangles, Lines, VLines, HLines -from .markers import Markers -from .text import Text from .util import contains -LAYERS = ("BACKGROUND", "UNDERLAY", "IMAGE", "DRAWING", "OVERLAY") +LAYERS = const(("BACKGROUND", "UNDERLAY", "IMAGE", "DRAWING", "OVERLAY")) class Surface: @@ -28,7 +26,6 @@ def draw(self, raster): clip = raster.clip(*object.clip) if clip is None: continue - print(object.clip, clip.x, clip.y, clip.w, clip.h, clip.offset, clip.stride) object.draw(clip.fbuf, clip.x, clip.y) def refresh(self, display, working_buffer): @@ -96,11 +93,13 @@ def hlines(self, layer, geometry, colors, clip=None): return shape def points(self, layer, geometry, colors, markers, clip=None): + from .markers import Markers points = Markers(geometry, colors, markers, clip=clip) self.add_shape(layer, points) return points def text(self, layer, geometry, colors, texts, bold=False, font=None, clip=None): + from .text import Text text = Text(geometry, colors, texts, bold=bold, font=font, clip=clip) self.add_shape(layer, text) return text diff --git a/src/tempe/surface.pyi b/src/tempe/surface.pyi new file mode 100644 index 0000000..5f0cf70 --- /dev/null +++ b/src/tempe/surface.pyi @@ -0,0 +1,165 @@ +"""Drawing surface class and related objects. + +This module defines the Surface class and related constants for using +it. + +Attributes +---------- + +Layers : tuple[str, ...] + The default set of layers used by Surfaces, in order from back to front. + The default value is ``("BACKGROUND", "UNDERLAY", "IMAGE", "DRAWING", "OVERLAY")`` + +""" +import asyncio +from array import array +from collections.abc import Iterable +from typing import Any + +from .display import Display +from .font import AbstractFont +from .geometry import Geometry +from .raster import Raster +from .shapes import Shape, Polygons, Rectangles, Lines, VLines, HLines, rectangle +from .util import contains + +#: The default set of layers used by Surfaces, in order from back to front. +LAYERS = ("BACKGROUND", "UNDERLAY", "IMAGE", "DRAWING", "OVERLAY") + +class Surface: + """A layered space for drawing shapes. + + This is the fundamental class where all Tempe drawing takes place. + A Surface has a number of layers, each of which holds zero or more + Shapes. The Surface draws shapes on order from the first layer to + the last, with each shape in a layer being drawn in the order it was + added to the Surface. + + Shapes which have changed update themselves and add the areas that + need re-drawing to the list of damaged regions. + + Actual drawing is carried out by the ``draw`` method, but most users of + Surface objects should call ``refresh``, which handles managing damaged + regions and clipping to minimise the actual work that's needed. + + Attributes + ---------- + refresh_needed : asyncio.Event + An Event that is set when the surface is damaged, and cleared + at the end of a ``refresh`` call. + """ + + def __init__(self): ... + + def refresh(self, display: Display, working_buffer: array[int]) -> None: + """Refresh the surface's appearance in the display. + + Calling this updates all damaged regions on the display device. + The caller should supply a chunk of memory as an array of unsigned + 16-bit integers that is used as a scratch space for partial + rendering. The larger this memory is (up to the size of the + display), the faster the display will be updated. + + Parameters + ---------- + display : Display + The actual physical display that the surface will be drawn on. + working_buffer : array.array[int] + An empty array of unsigned 16-bit ints (ie. ``array('H', ...)``) + that the Surface will use as memory for temporary drawing buffers. + """ + + def add_shape(self, layer: Any, shape: Shape) -> None: + """Add a shape to a layer of the drawing.""" + + def remove_shape(self, layer: Any, shape: Shape) -> None: ... + + def clear(self, layer: Any) -> None: + """Clear all shapes from a layer.""" + + def damage(self, rect: rectangle) -> None: + """Add a rectangle to the regions which need updating. + + This also sets the ``refresh_needed`` event. + + This should be called by Shape classes when they are changed. + + Parameters + ---------- + rect : tempe.shapes.rectangle + A rectangle in the form of a tuple (x, y, w, h) which contains + a region where the shapes being displayed have changed. + """ + + def draw(self, raster: Raster) -> None: + """Draw the contents of the surface onto the Raster. + + Most end-user code should call ``refresh`` instead. + + Parameters + ---------- + raster : Raster + The Raster object that the shapes will be drawn on. + """ + + def polys( + self, + layer: Any, + geometry: Geometry[array], + colors: Iterable[int], + clip: tuple[int, int, int, int] | None = None, + ) -> Polygons: ... + + def rects( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + clip: tuple[int, int, int, int] | None = None, + ) -> Rectangles: ... + + def lines( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + clip: tuple[int, int, int, int] | None = None, + ) -> Lines: ... + + def vlines( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + clip: tuple[int, int, int, int] | None = None, + ) -> VLines: ... + + def hlines( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + clip: tuple[int, int, int, int] | None = None, + ) -> HLines: ... + + def points( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + markers: Iterable[Any], + clip: tuple[int, int, int, int] | None = None, + ) -> "tempe.markers.Markers": ... + + def text( + self, + layer: Any, + geometry: Geometry[tuple[int, int, int, int]], + colors: Iterable[int], + text: Iterable[str], + bold: bool = False, + font: AbstractFont | None = None, + clip: tuple[int, int, int, int] | None = None, + ) -> "tempe.text.Text": ... + +__all__ = ["Surface"] diff --git a/src/tempe/text.py b/src/tempe/text.py index d5595bb..32303eb 100644 --- a/src/tempe/text.py +++ b/src/tempe/text.py @@ -1,7 +1,7 @@ from array import array import framebuf -from .font import BitmapFont +from .font import BitmapFont, AlrightFont from .shapes import ColoredGeometry, BLIT_KEY_RGB565 @@ -43,11 +43,21 @@ def draw(self, buffer, x=0, y=0): buf, width, height, framebuf.MONO_HLSB ) buffer.blit(fbuf, px, py, BLIT_KEY_RGB565, palette) - if self.bold: - px += 1 - buffer.blit(fbuf, px, py, BLIT_KEY_RGB565, palette) px += width py += line_height + # elif isinstance(self.font, AlrightFont): + # line_height = self.font.height + # for geometry, color, text in self: + # py = geometry[1] - y + # for i, line in enumerate(text.splitlines()): + # px = geometry[0] - x + # for char in line: + # contours = self.font.contours(char) + # for contour in contours: + # print(contour) + # buffer.poly(px, py, contour, color, True) + # px += self.font.measure(char)[2] + # py += line_height def update(self, geometry=None, colors=None, texts=None): if geometry is not None: diff --git a/src/tempe/text.pyi b/src/tempe/text.pyi new file mode 100644 index 0000000..2305d24 --- /dev/null +++ b/src/tempe/text.pyi @@ -0,0 +1,35 @@ + +from collections.abc import Iterable +from typing import Any +import framebuf + + +from .geometry import Geometry +from .font import AbstractFont +from .shapes import ColoredGeometry, BLIT_KEY_RGB565, rectangle, point +from .surface import Surface + + +class Text(ColoredGeometry[tuple[int, int]]): + + def __init__( + self, + geometry: Iterable[point], + colors: Iterable[int], + texts: Iterable[str], + *, + bold: bool = False, + font: AbstractFont | None = None, + surface: Surface | None = None, + clip: rectangle | None = None, + ): ... + + def __iter__(self) -> tuple[Geometry[tuple[int, int]], int, str]: ... + + def update( + self, + geometry: Iterable[tuple[int, int]] | None = None, + colors: Iterable[int] | None = None, + texts: Iterable[str] | None = None, + **kwargs: Any, + ): ... diff --git a/src/tempe_font/__init__.py b/src/tempe_font/__init__.py deleted file mode 100644 index e69de29..0000000