Source code for canvascli.canvas

# -*- coding: utf-8 -*-
"""Drawing Canvas."""

# Standard library imports
from collections import deque

# Local imports
from canvascli.utils import check_color


[docs]class Canvas(object): """ Drawing Canvas. An empty canvas can be defined but it needs to be created to allow drawing. These canvas is based on 1-indexing, so all coordinates (x, y points) for drawing must be greater than 1. The internal representation of the canvas is based on 0-indexing like all python containers and uses a list of lists. Notes: ------ - This canvas currently only support an integer coordinate system. - Currently only horizontal or vertical lines are supported. - Horizontal and vertical lines will be drawn using 'x' - The horizontal and vertical border of the canvas will be drawn using '-' and '|' respectively. """ MARKER = 'x' HBORDER = '-' VBORDER = '|' DEFAULT_COLOR = 'o' def __init__(self, width=None, height=None): """Drawing Canvas.""" self._width = width self._height = height self._canvas = None # Canvas is defined as a lists of lists using 0-based indexing if width is not None and height is not None: self.create(width, height) elif width or height: raise TypeError('Must provide both with and height arguments!') # --- Magic methods # ------------------------------------------------------------------------- def __str__(self): """String representation of the Canvas.""" if self._canvas is not None: data = self._top_bottom + '\n' for row_values in self._canvas: data += self.VBORDER for value in row_values: data += ' ' if value is None else value data += self.VBORDER + '\n' data += self._top_bottom else: data = '[]' return data def __unicode__(self): """String representation of the Canvas.""" return self.__str__() def __repr__(self): """Representation of the Canvas.""" canvas = self._canvas if self._canvas is not None else [] return repr(canvas) # --- Helper methods # ------------------------------------------------------------------------- def _check_canvas(self): """Check if canvas has been initialized.""" if self._canvas is None: raise Exception('Must create the canvas first!') def _check_point(self, x_coord, y_coord): """ Check that point is a positive integer located within the canvas. If the coordinates are not posistve integers an exception is raised. If the coordinates are outside the canvas a warning is emitted. """ if not (isinstance(x_coord, int) and x_coord > 0 and isinstance(x_coord, int) and y_coord > 0): raise TypeError('Point ({}, {}) must have positive integer ' 'coordinates!'.format(x_coord, y_coord)) check_x = x_coord >= 1 and x_coord <= self._width check_y = y_coord >= 1 and y_coord <= self._height check = check_x and check_y if not check: raise ValueError('Point ({}, {}) is outside of canvas!' ''.format(x_coord, y_coord)) return check def _draw_hline(self, x1, x2, y): """Draw horizontal line on canvas on 0-based indexing.""" self._canvas[y][x1:x2+1] = [self.MARKER]*(x2-x1+1) def _draw_vline(self, y1, y2, x): """Draw vertical line on canvas on 0-based indexing.""" for y in range(y1, y2+1): self._canvas[y][x] = self.MARKER def _fill_helper(self, queue, color, x_i, y_i, dx=0, dy=0): """ Check pixel located at xi+dx and yi+dy for color and add to fill queue. """ x_dx, y_dy = x_i + dx, y_i + dy value = self._canvas[y_dy][x_dx] if value != color and value != self.MARKER: self._canvas[y_dy][x_dx] = color queue.append((x_dx, y_dy)) # --- Canvas API methods # -------------------------------------------------------------------------
[docs] def create(self, width, height): """Create the canvas with a given width and height.""" # Check input if not (isinstance(width, int) and width > 0 and isinstance(height, int) and height > 0): raise TypeError("Must provide a positive integer for the width " " and the height!") self._width = width self._height = height # Top and bottom canvas boder used for printing self._top_bottom = self.HBORDER*(width + 2) self._canvas = [[None]*width for item in range(height)]
[docs] def draw_line(self, x1, y1, x2, y2): """Draw horizontal or vertical line on canvas on 1-based indexing.""" self._check_canvas() if self._check_point(x1, y1) and self._check_point(x2, y2): if x1 != x2 and y1 != y2: raise ValueError('Only horizontal or vertical lines are ' 'currently supported') upper_left, bottom_right = sorted([(x1, y1), (x2, y2)]) if x1 == x2: self._draw_vline(upper_left[1]-1, bottom_right[1]-1, x1-1) elif y1 == y2: self._draw_hline(upper_left[0]-1, bottom_right[0]-1, y1-1)
[docs] def draw_rectangle(self, x1, y1, x2, y2): """Draw rectangle on canvas on 1-based indexing.""" self._check_canvas() if self._check_point(x1, y1) and self._check_point(x1, y1): self.draw_line(x1, y1, x2, y1) self.draw_line(x1, y2, x2, y2) self.draw_line(x1, y1, x1, y2) self.draw_line(x2, y1, x2, y2)
[docs] def fill(self, x, y, color=DEFAULT_COLOR): """ Fill the entire area connected to `x` and `y` with `color`. Notes ----- - This method uses the "Forest Fire" algorithm. Could be improved, but should suffice for this application. """ self._check_canvas() color = check_color(color) if self._check_point(x, y): x_o, y_o = x - 1, y - 1 value = self._canvas[y_o][x_o] if value != color and value != self.MARKER: queue = deque() # Using deque as Queue as a convenience self._fill_helper(queue, color, x_o, y_o) while queue: node = queue.pop() x_i, y_i = node if x_i - 1 > -1: self._fill_helper(queue, color, x_i, y_i, dx=-1) if y_i - 1 > -1: self._fill_helper(queue, color, x_i, y_i, dy=-1) if x_i + 1 < self._width: self._fill_helper(queue, color, x_i, y_i, dx=1) if y_i + 1 < self._height: self._fill_helper(queue, color, x_i, y_i, dy=1)
[docs] def erase(self): """Erase the contents of the canvas.""" self._check_canvas() self.create(self._width, self._height)
# --- Properties # ------------------------------------------------------------------------- @property def data(self): """Return the list of list holding the canvas data values..""" return self._canvas @property def width(self): """Return the current width of the canvas, or None if not set.""" return self._width @property def height(self): """Return the current height of the canvas, or None if not set.""" return self._height