Created
November 16, 2023 09:47
-
-
Save encukou/5e28e1e9feb49be48f72860c21126931 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import unicodedata | |
def get_components(char): | |
name = unicodedata.name(char).removeprefix('BOX DRAWINGS ') | |
result = [] | |
for part in name.split(' AND '): | |
part = part.replace(' DASH', '-DASH') | |
directions = [] | |
for word in part.split(): | |
match word: | |
case 'LIGHT' | 'HEAVY': | |
style = word | |
case ('DOUBLE' | 'DOUBLE-DASH' | 'TRIPLE-DASH' | |
| 'QUADRUPLE-DASH'): | |
style = word | |
case 'SINGLE': | |
style = 'LIGHT' | |
case 'DASH': | |
pass | |
case 'LEFT' | 'RIGHT' | 'UP' | 'DOWN': | |
directions.append(word) | |
case 'HORIZONTAL': | |
directions.extend(['LEFT', 'RIGHT']) | |
case 'VERTICAL': | |
directions.extend(['UP', 'DOWN']) | |
case 'ARC' | 'DIAGONAL': | |
# We don't compose these | |
return None | |
case _: | |
raise ValueError(word) | |
result.extend((style, d) for d in directions) | |
return frozenset(result) | |
BOX_CHARS = tuple({chr(n) for n in range(0x2500, 0x2580)}) | |
BOX_CHARS_BY_COMPONENT = {get_components(c): c for c in BOX_CHARS} | |
del BOX_CHARS_BY_COMPONENT[None] | |
BOX_CHARS_COMPONENTS = {v: k for k, v in BOX_CHARS_BY_COMPONENT.items()} | |
class Canvas: | |
def __init__(self, contents=()): | |
self.contents = {} | |
if contents: | |
self.update(contents) | |
self._bounds = None | |
def __setitem__(self, row_col, symbol): | |
row, col = row_col | |
self.contents[row_col] = symbol | |
self._bounds = None | |
def update(self, items): | |
if isinstance(items, Canvas): | |
self.update(items.contents) | |
else: | |
for coords, char in items.items(): | |
row, col = coords | |
if ((c1 := BOX_CHARS_COMPONENTS.get(char)) | |
and (c2 := BOX_CHARS_COMPONENTS.get( | |
self.contents.get(coords)))): | |
self[coords] = BOX_CHARS_BY_COMPONENT.get(c1 | c2, char) | |
else: | |
self[coords] = char | |
self._bounds = None | |
def blit(self, other, row, col): | |
self.update({ | |
(row+r, col+c): char | |
for (r, c), char in other.contents.items() | |
}) | |
def shift(self, row, col): | |
self.contents = { | |
(row+r, col+c): char | |
for (r, c), char in self.contents.items() | |
} | |
self._bounds = None | |
def draw_text(self, message, row=0, col=0): | |
for c, char in enumerate(message): | |
self[row, col + c] = char | |
def draw_hline(self, row, col1, col2, style='LIGHT', chars=None): | |
if chars is None: | |
chars = ( | |
BOX_CHARS_BY_COMPONENT[frozenset({(style, 'RIGHT')})] | |
+ BOX_CHARS_BY_COMPONENT[frozenset({(style, 'LEFT'), (style, 'RIGHT')})] | |
+ BOX_CHARS_BY_COMPONENT[frozenset({(style, 'LEFT')})] | |
) | |
self.update({ | |
(row, col1): chars[0], | |
**{(row, c): chars[1] for c in range(col1 + 1, col2)}, | |
(row, col2): chars[2], | |
}) | |
def draw_vline(self, row1, row2, col, style='LIGHT', chars=None): | |
if chars is None: | |
chars = ( | |
BOX_CHARS_BY_COMPONENT[frozenset({(style, 'DOWN')})] | |
+ BOX_CHARS_BY_COMPONENT[frozenset({(style, 'DOWN'), (style, 'UP')})] | |
+ BOX_CHARS_BY_COMPONENT[frozenset({(style, 'UP')})] | |
) | |
self.update({ | |
(row1, col): chars[0], | |
**{(r, col): chars[1] for r in range(row1 + 1, row2)}, | |
(row2, col): chars[2], | |
}) | |
def draw_box(self, row1, col1, row2, col2, style='LIGHT', chars=None): | |
if chars is None: | |
self.draw_hline(row1, col1, col2, style) | |
self.draw_hline(row2, col1, col2, style) | |
self.draw_vline(row1, row2, col1, style) | |
self.draw_vline(row1, row2, col2, style) | |
else: | |
self.draw_hline(row1, col1, col2, chars=chars[0:3]) | |
self.draw_hline(row2, col1, col2, chars=chars[6:9]) | |
self.draw_vline(row1+1, row2-1, col1, chars=chars[3]*3) | |
self.draw_vline(row1+1, row2-1, col2, chars=chars[5]*3) | |
def _get_bounds(self): | |
if self._bounds is None: | |
if self.contents: | |
rows = sorted(set(row for row, col in self.contents)) | |
cols = sorted(set(col for row, col in self.contents)) | |
self._bounds = rows[0], cols[0], rows[-1], cols[-1] | |
else: | |
self._bounds = 0, 0, 0, 0 | |
return self._bounds | |
@property | |
def min_row(self): | |
return self._get_bounds()[0] | |
@property | |
def min_col(self): | |
return self._get_bounds()[1] | |
@property | |
def max_row(self): | |
return self._get_bounds()[2] | |
@property | |
def max_col(self): | |
return self._get_bounds()[3] | |
@property | |
def height(self): | |
bounds = self._get_bounds() | |
return bounds[2] - bounds[0] | |
@property | |
def width(self): | |
bounds = self._get_bounds() | |
return bounds[3] - bounds[1] | |
def dump(self): | |
for row in range(self.min_row, self.max_row + 1): | |
for col in range(self.min_col, self.max_col + 1): | |
print(self.contents.get((row, col), ' '), end='') | |
print() | |
canvas = Canvas() | |
canvas.draw_box(0, 0, 3, 5) | |
canvas.draw_box(1, 1, 4, 8, style='HEAVY') | |
canvas.dump() | |
#result: | |
''' | |
┌────┐ | |
│┏━━━┿━━┓ | |
│┃ │ ┃ | |
└╂───┘ ┃ | |
┗━━━━━━┛ | |
''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment