From 3e3b3543b233fcafa40dbb6d9e7510f297bfe15c Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 1 Aug 2020 22:46:35 -0700 Subject: [PATCH 1/3] uhm --- RAIILock.py | 35 +++++++++ TextCanvas.py | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 RAIILock.py diff --git a/RAIILock.py b/RAIILock.py new file mode 100644 index 0000000..f570e0c --- /dev/null +++ b/RAIILock.py @@ -0,0 +1,35 @@ + + +import threading + + +class RAIILock: + + def __init__(self, lock: threading.RLock, defer=False): + + self.__lock = lock + self.__defer = defer + + self.__acquired = False + + def __enter__(self): + + if self.__defer is False: + self.acquire() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + self.release() + + def acquire(self): + + self.__lock.acquire() + self.__acquired = True + + def release(self): + + if self.__acquired: + self.__lock.release() + self.__acquired = False diff --git a/TextCanvas.py b/TextCanvas.py index e69de29..78c7827 100644 --- a/TextCanvas.py +++ b/TextCanvas.py @@ -0,0 +1,202 @@ + + +from .RAIILock import RAIILock + + +import math +import os +import threading +import time + + +class TextCanvas: + + __DEFAULT_TERMINAL_WIDTH = 80 + __DEFAULT_TERMINAL_HEIGHT = 22 + __TERMINAL_UPDATE_INTERVAL = 1 + + __DRAW_RESOLUTION = 3 + + def __init__(self): + + self.__terminal_width = self.__DEFAULT_TERMINAL_WIDTH + self.__terminal_height = self.__DEFAULT_TERMINAL_HEIGHT + self.__last_terminal_dimensions_update = time.time() + self._update_terminal_dimensions(force=True) + + self.__grid = [] + self.__render_string = "" + self.__is_dirty = True + self.__last_render_time = time.time() + + self.__lock = threading.RLock() + + def _update_render_if_dirty(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + if self.__is_dirty: + self._update_render(acquire_locks=False) + + def _update_render(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + self.clear(acquire_locks=acquire_locks) + self._update_render_string(acquire_locks=acquire_locks) + + self.__last_render_time = time.time() + + self.__is_dirty = False + + def _update_render_string(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + s = "" + for line in self.__grid: + for c in line: + s += c + s += "\n" + + # Don't need that last line feed + s = s[:-1] + + self.__grid_string = s + + def get(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + self._update_render_if_dirty(acquire_locks=acquire_locks) + + return self.__grid_string + + def print(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + s = self.get(acquire_locks=acquire_locks) + + print(s) + + def draw_text(self, x, y, text, center=False, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + if not isinstance(text, str): + text = str(text) + + if len(text) == 0: + return + + if center is True: + x -= len(text) / 2 + x = int(x) + + for c in text: + self.write(x=x, y=y, character=c, acquire_locks=acquire_locks) + x += 1 + + def draw_line(self, start_x, start_y, end_x, end_y, character="*", acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + distance_x = math.fabs(end_x - start_x) + distance_y = math.fabs(end_y - start_y) + if distance_x > distance_y: + distance = distance_x + else: + distance = distance_y + + direction_x = 1 if end_x > start_x else -1 + direction_y = 1 if end_y > start_y else -1 + + steps = round(distance * self.__DRAW_RESOLUTION) + if steps == 0: + return + + increment_x = distance_x / steps + increment_x *= direction_x + + increment_y = distance_y / steps + increment_y *= direction_y + + pos_x = start_x + pos_y = start_y + for i in range(steps): + + pos_x_rounded = round(pos_x) + pos_y_rounded = round(pos_y) + + self.write(x=pos_x_rounded, y=pos_y_rounded, character=character, acquire_locks=acquire_locks) + + pos_x += increment_x + pos_y += increment_y + + def write(self, x, y, character, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + x = round(x) + y = round(y) + + if 0 <= y < len(self.__grid): + if 0 <= x < len(self.__grid[y]): + + self.__grid[y][x] = character + + self.__is_dirty = True + + def clear(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + self._update_terminal_dimensions(acquire_locks=acquire_locks) + + self.__grid = [] + + for y in range(self.__terminal_height): + + self.__grid.append([]) + + for x in range(self.__terminal_width): + + self.__grid[y].append(" ") + + self.__is_dirty = True + + def get_dimensions(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + height = len(self.__grid) + if height == 0: + width = 0 + else: + width = len(self.__grid[0]) + + return width, height + + def _update_terminal_dimensions(self, force=False, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + now = time.time() + + if force is not False and now - self.__last_terminal_dimensions_update < self.__TERMINAL_UPDATE_INTERVAL: + return + self.__last_terminal_dimensions_update = now + + width, height = self.determine_terminal_dimensions() + + self.__terminal_width = width + self.__terminal_height = height + + @staticmethod + def determine_terminal_dimensions(): + + columns, lines = os.get_terminal_size() + + return columns, lines + From 978c6d9d8c8ddb2c5cf28a3e747930bd2c739c8d Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 2 Aug 2020 00:20:36 -0700 Subject: [PATCH 2/3] Lots of fun stuff, including virtual dimensions to help reduce warping of grid when printed to a terminal --- TextCanvas.py | 161 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 31 deletions(-) diff --git a/TextCanvas.py b/TextCanvas.py index 78c7827..91270d8 100644 --- a/TextCanvas.py +++ b/TextCanvas.py @@ -11,25 +11,66 @@ import time class TextCanvas: - __DEFAULT_TERMINAL_WIDTH = 80 - __DEFAULT_TERMINAL_HEIGHT = 22 + __DEFAULT_WIDTH = 80 + __DEFAULT_HEIGHT = 22 __TERMINAL_UPDATE_INTERVAL = 1 - __DRAW_RESOLUTION = 3 + __BACKGROUND_CHARACTER = " " - def __init__(self): + __DRAW_RESOLUTION = 3 + __DEFAULT_TEXT_HEIGHT_TO_WIDTH_RATIO = 2.0 + + def __init__( + self, + output_width=None, output_height=None, text_to_height_width=None, + auto_adjust_to_terminal=True, + print_upward=True + ): - self.__terminal_width = self.__DEFAULT_TERMINAL_WIDTH - self.__terminal_height = self.__DEFAULT_TERMINAL_HEIGHT + self.__lock = threading.RLock() + + # Real dimensions + if output_width is None: + output_width = self.__DEFAULT_WIDTH + if output_height is None: + output_height = self.__DEFAULT_HEIGHT + self.__grid_width = output_width + self.__grid_height = output_height + + # Virtual dimensions + if text_to_height_width is None: + text_to_height_width = self.__DEFAULT_TEXT_HEIGHT_TO_WIDTH_RATIO + self.__text_to_height_width = text_to_height_width + self.__virtual_width = None + self.__virtual_height = None + self._auto_calculate_virtual_dimensions(acquire_locks=False) + self._update_virtual_ratios(acquire_locks=False) + + # Terminal stuff + self.__terminal_width = 0 + self.__terminal_height = 0 + self.__auto_adjust_to_terminal = auto_adjust_to_terminal self.__last_terminal_dimensions_update = time.time() - self._update_terminal_dimensions(force=True) + if self.__auto_adjust_to_terminal: + self._update_terminal_dimensions(force=True) + # + self.__print_upward = print_upward + + # Actual grid self.__grid = [] self.__render_string = "" self.__is_dirty = True self.__last_render_time = time.time() + + def _update_virtual_ratios(self, acquire_locks=True): - self.__lock = threading.RLock() + with RAIILock(self.__lock, defer=not acquire_locks): + + self.__virtual_height_per_grid_height = self.__virtual_height / self.__grid_height + self.__virtual_width_per_grid_width = self.__virtual_width / self.__grid_width + + print("Virtual ratios:", self.__virtual_width_per_grid_width, self.__virtual_height_per_grid_height) def _update_render_if_dirty(self, acquire_locks=True): @@ -42,11 +83,9 @@ class TextCanvas: with RAIILock(self.__lock, defer=not acquire_locks): - self.clear(acquire_locks=acquire_locks) - self._update_render_string(acquire_locks=acquire_locks) + self._update_render_string(acquire_locks=False) self.__last_render_time = time.time() - self.__is_dirty = False def _update_render_string(self, acquire_locks=True): @@ -62,21 +101,31 @@ class TextCanvas: # Don't need that last line feed s = s[:-1] - self.__grid_string = s + self.__render_string = s def get(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): - self._update_render_if_dirty(acquire_locks=acquire_locks) + self._update_render_if_dirty(acquire_locks=False) - return self.__grid_string + return self.__render_string def print(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): - s = self.get(acquire_locks=acquire_locks) + self._update_render_if_dirty(acquire_locks=False) + + s = "" + + # Move the cursor up to compensate + if self.__print_upward: + up_count = self.__grid_height + s += ("\033[F" * up_count) + + # Grab render + s += self.get(acquire_locks=False) print(s) @@ -95,7 +144,7 @@ class TextCanvas: x = int(x) for c in text: - self.write(x=x, y=y, character=c, acquire_locks=acquire_locks) + self.write(x=x, y=y, character=c, acquire_locks=False) x += 1 def draw_line(self, start_x, start_y, end_x, end_y, character="*", acquire_locks=True): @@ -129,7 +178,7 @@ class TextCanvas: pos_x_rounded = round(pos_x) pos_y_rounded = round(pos_y) - self.write(x=pos_x_rounded, y=pos_y_rounded, character=character, acquire_locks=acquire_locks) + self.write(x=pos_x_rounded, y=pos_y_rounded, character=character, acquire_locks=False) pos_x += increment_x pos_y += increment_y @@ -138,8 +187,7 @@ class TextCanvas: with RAIILock(self.__lock, defer=not acquire_locks): - x = round(x) - y = round(y) + x, y = self._translate_virtual_to_internal_coordinates(x=x, y=y) if 0 <= y < len(self.__grid): if 0 <= x < len(self.__grid[y]): @@ -152,17 +200,17 @@ class TextCanvas: with RAIILock(self.__lock, defer=not acquire_locks): - self._update_terminal_dimensions(acquire_locks=acquire_locks) + self._auto_adjust_to_terminal(acquire_locks=False) self.__grid = [] - for y in range(self.__terminal_height): + for y in range(self.__grid_height): self.__grid.append([]) - for x in range(self.__terminal_width): + for x in range(self.__grid_width): - self.__grid[y].append(" ") + self.__grid[y].append(self.__BACKGROUND_CHARACTER) self.__is_dirty = True @@ -170,13 +218,63 @@ class TextCanvas: with RAIILock(self.__lock, defer=not acquire_locks): - height = len(self.__grid) - if height == 0: - width = 0 - else: - width = len(self.__grid[0]) + return self.get_virtual_dimensions() + + def get_virtual_dimensions(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): - return width, height + return self.__virtual_width, self.__virtual_height + + def _get_internal_dimensions(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + return self.__grid_width, self.__grid_height + + def _translate_virtual_to_internal_coordinates(self, x, y): + + x_translated = x / self.__virtual_width_per_grid_width + y_translated = y / self.__virtual_height_per_grid_height + + x_translated = round(x_translated) + y_translated = round(y_translated) + + return x_translated, y_translated + + def _is_time_to_update_terminal_dimensions(self): + + if self.__auto_adjust_to_terminal: + + now = time.time() + + if now - self.__last_terminal_dimensions_update > self.__TERMINAL_UPDATE_INTERVAL: + return True + + return False + + def _auto_adjust_to_terminal(self, force=False, acquire_locks=True): + + if not force and not self.__auto_adjust_to_terminal: + return + + with RAIILock(self.__lock, defer=not acquire_locks): + + self._update_terminal_dimensions(acquire_locks=False) + + self.__grid_width = self.__terminal_width + self.__grid_height = self.__terminal_height + + self._auto_calculate_virtual_dimensions(acquire_locks=False) + + def _auto_calculate_virtual_dimensions(self, acquire_locks=True): + + with RAIILock(self.__lock, defer=not acquire_locks): + + self.__virtual_width = self.__grid_width + self.__virtual_height = self.__grid_height * self.__text_to_height_width + + self._update_virtual_ratios(acquire_locks=False) def _update_terminal_dimensions(self, force=False, acquire_locks=True): @@ -184,7 +282,7 @@ class TextCanvas: now = time.time() - if force is not False and now - self.__last_terminal_dimensions_update < self.__TERMINAL_UPDATE_INTERVAL: + if force is not False and not self._is_time_to_update_terminal_dimensions(): return self.__last_terminal_dimensions_update = now @@ -192,6 +290,8 @@ class TextCanvas: self.__terminal_width = width self.__terminal_height = height + + self._auto_calculate_virtual_dimensions(acquire_locks=False) @staticmethod def determine_terminal_dimensions(): @@ -199,4 +299,3 @@ class TextCanvas: columns, lines = os.get_terminal_size() return columns, lines - From f8c13c0c2d214534cfed1fc05bbc36c206baf81b Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 2 Aug 2020 01:38:33 -0700 Subject: [PATCH 3/3] remove debug out --- TextCanvas.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/TextCanvas.py b/TextCanvas.py index 91270d8..d0c33f2 100644 --- a/TextCanvas.py +++ b/TextCanvas.py @@ -69,8 +69,6 @@ class TextCanvas: self.__virtual_height_per_grid_height = self.__virtual_height / self.__grid_height self.__virtual_width_per_grid_width = self.__virtual_width / self.__grid_width - - print("Virtual ratios:", self.__virtual_width_per_grid_width, self.__virtual_height_per_grid_height) def _update_render_if_dirty(self, acquire_locks=True):