Source code for arrowline

# SPDX-FileCopyrightText: Copyright (c) 2021 Jose David
#
# SPDX-License-Identifier: MIT
"""
`arrowline`
================================================================================

Utility function to draw arrow lines using vectorio and tilegride to display it


* Author(s): Jose David M

Implementation Notes
--------------------

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""

import math
import displayio
from vectorio import Polygon, Circle

try:
    from typing import Optional
except ImportError:
    pass

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/jposada202020/CircuitPython_ArrowLine.git"


[docs] class Line: """A Line Arrow utility. :param grid: Tilegrid object where the bitmap will be located, set to None for arbitrary placement of the :class:`Line` (default = None) :param int x1: line first point x coordinate :param int y1: line first point x coordinate :param int x2: line first point x coordinate :param int y2: line first point x coordinate :param int arrow_length: arrow length in pixels. Arrow width is half of the length :param `displayio.Palette` palette: palette object used to display the bitmap. This is used to have the same color for the arrow :param int pal_index: pallet color index used in the bitmap to give the arrow line the color property :param int line_width: the width of the arrow's line, in pixels (default = 1) :param bool solid_line: indicates if the line is a solid line. Defaults to `True` :param int line_length: Length in pixels of the line. combinations of line_length and line_space. Defaults to 5. :param int line_space: Line space in pixels. Defaults to 5 :param str pointer: point type. Two pointers could be selected :const:`C` Circle or :const:`A` Arrow. Defaults to Arrow :return: `displayio.Group` **Quickstart: Importing and using line_arrow** Here is one way of importing the :class:`Line` class so you can use: .. code-block:: python import displayio import board from CircuitPython_ArrowLine import Line display = board.DISPLAY my_group = displayio.Group() bitmap = displayio.Bitmap(100, 100, 5) screen_palette = displayio.Palette(3) screen_palette[1] = 0x00AA00 screen_tilegrid = displayio.TileGrid( bitmap, pixel_shader=screen_palette, x=50, y=50, ) my_group.append(screen_tilegrid) Now you can create an arrowline starting at pixel position x=40, y=90 using: .. code-block:: python my_line = Line(screen_tilegrid, bitmap, 40, 90, 90, 60, 12, screen_palette, 1) Once you setup your display, you can now add ``my_line`` to your display using: .. code-block:: python my_group.append(line) display.show(my_group) **Summary: `arrowline` Features and input variables** The :class:`Line` widget has some options for controlling its position, visible appearance, and scale through a collection of input variables: - **position**: :const:`x1`, :const:`y1`, :const:`x2`, :const:`y2` - **size**: line length is given by two points. :const:`arrow_length` - **color**: :const:`pal_index` - **background color**: :const:`background_color` """ def __init__( self, grid: Optional[displayio.TileGrid] = None, x1: int = 0, y1: int = 0, x2: int = 10, y2: int = 10, arrow_length: int = 10, palette: Optional[displayio.Palette] = None, pal_index: int = 1, line_width: int = 1, solid_line: bool = True, line_length: int = 5, line_space: int = 5, pointer: str = "A", ) -> None: if palette is None: raise ValueError("Must provide a valid palette") self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 self._arrow_length = arrow_length self.line_width = line_width self.line_length = line_length self.line_space = line_space self.my_group = displayio.Group() self.arrow_palette = displayio.Palette(2) self.arrow_palette[1] = palette[pal_index] if grid is not None: self.x_reference = grid.x self.y_reference = grid.y else: self.x_reference = 0 self.y_reference = 0 self._angle = math.atan2((y2 - y1), (x2 - x1)) angle2 = math.pi / 2 - self._angle arrow_side_x = arrow_length // 2 * math.cos(angle2) arrow_side_y = arrow_length // 2 * math.sin(angle2) self.x0 = int(math.ceil(arrow_length * math.cos(self._angle))) self.y0 = int(math.ceil(arrow_length * math.sin(self._angle))) start_x = self.x_reference + self.x2 start_y = self.y_reference + self.y2 self._arrow_base_x = start_x - self.x0 self._arrow_base_y = start_y - self.y0 self._distance = math.sqrt( ((self.x2 - self.x0) - x1) ** 2 + ((self.y2 - self.y0) - y1) ** 2 ) right_x = math.ceil(self._arrow_base_x + arrow_side_x) right_y = math.ceil(self._arrow_base_y - arrow_side_y) left_x = math.ceil(self._arrow_base_x - arrow_side_x) left_y = math.ceil(self._arrow_base_y + arrow_side_y) end_line_x = self.x2 - self.x0 end_line_y = self.y2 - self.y0 self.line_draw = _angledrectangle( self.x1, self.y1, end_line_x, end_line_y, stroke=self.line_width ) if pointer == "A": arrow = Polygon( pixel_shader=self.arrow_palette, points=[(start_x, start_y), (right_x, right_y), (left_x, left_y)], x=0, y=0, color_index=1, ) self.my_group.append(arrow) elif pointer == "C": circle_center_x = self.x_reference + self.line_draw[2][0] circle_center_y = self.y_reference + self.line_draw[2][1] circle_ending = Circle( pixel_shader=self.arrow_palette, radius=3, x=circle_center_x, y=circle_center_y, color_index=1, ) self.my_group.append(circle_ending) if solid_line: self._solid_line() else: self._dotted_line() @property def draw(self) -> None: """ Return the line object """ return self.my_group def _solid_line(self) -> None: line_base = Polygon( pixel_shader=self.arrow_palette, points=[ ( self.x_reference + self.line_draw[0][0], self.y_reference + self.line_draw[0][1], ), ( self.x_reference + self.line_draw[1][0], self.y_reference + self.line_draw[1][1], ), ( self.x_reference + self.line_draw[2][0], self.y_reference + self.line_draw[2][1], ), ( self.x_reference + self.line_draw[3][0], self.y_reference + self.line_draw[3][1], ), ], x=0, y=0, color_index=1, ) self.my_group.append(line_base) def _dotted_line(self) -> None: distance = math.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) suma = self.line_length puntos = [] puntos.append((self.x2, self.y2)) while suma < distance: puntos.append( ( self.x2 - int(math.ceil(suma * math.cos(self._angle))), self.y2 - int(math.ceil(suma * math.sin(self._angle))), ) ) suma = suma + self.line_length + self.line_space for i, ele in enumerate(puntos): if i == 0: continue if i % 2 == 0: _line = _angledrectangle( ele[0], ele[1], puntos[i - 1][0], puntos[i - 1][1], stroke=self.line_width, ) line_base = Polygon( pixel_shader=self.arrow_palette, points=[ ( self.x_reference + _line[0][0], self.y_reference + _line[0][1], ), ( self.x_reference + _line[1][0], self.y_reference + _line[1][1], ), ( self.x_reference + _line[2][0], self.y_reference + _line[2][1], ), ( self.x_reference + _line[3][0], self.y_reference + _line[3][1], ), ], x=0, y=0, color_index=1, ) self.my_group.append(line_base)
def _angledrectangle(x1, y1, x2, y2, stroke=1): # Code Source for this function by kmatch98 (R) 2021 # https://github.com/adafruit/CircuitPython_Community_Bundle/pull/63 if x2 - x1 == 0: xdiff1 = round(stroke / 2) xdiff2 = -round(stroke - xdiff1) ydiff1 = 0 ydiff2 = 0 elif y2 - y1 == 0: xdiff1 = 0 xdiff2 = 0 ydiff1 = round(stroke / 2) ydiff2 = -round(stroke - ydiff1) else: c_dist = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) xdiff = stroke * (y2 - y1) / c_dist xdiff1 = round(xdiff / 2) xdiff2 = -round(xdiff - xdiff1) ydiff = stroke * (x2 - x1) / c_dist ydiff1 = round(ydiff / 2) ydiff2 = -round(ydiff - ydiff1) return [ (x1 + xdiff1, y1 + ydiff2), (x1 + xdiff2, y1 + ydiff1), (x2 + xdiff2, y2 + ydiff1), (x2 + xdiff1, y2 + ydiff2), ]