Skip to content

Instantly share code, notes, and snippets.

@encukou
Created November 16, 2023 09:47
Show Gist options
  • Save encukou/5e28e1e9feb49be48f72860c21126931 to your computer and use it in GitHub Desktop.
Save encukou/5e28e1e9feb49be48f72860c21126931 to your computer and use it in GitHub Desktop.
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