|
|
@ -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 |
|
|
|
|