1010
1111import enum
1212import dataclasses
13+
1314import numpy as np
15+ import einops
1416
1517from typing import Self
16- from numpy .typing import ArrayLike
18+ from numpy .typing import NDArray , ArrayLike
1719
1820from matthewplotlib .colors import Color
1921from matthewplotlib .unscii16 import bitmap
@@ -52,7 +54,7 @@ def bg_(self: Self) -> Color | None:
5254 * If c happens to be '█' or '▟', then it is more effective to return
5355 the foreground color.
5456 * If c happens to be '▀' or '▄', then it is more effective to return a
55- mixture of the two colours, presuming there are two colours to mix.
57+ mixture of the two colours (when there are two colours to mix) .
5658
5759 TODO:
5860
@@ -141,23 +143,25 @@ def to_rgba_array(
141143], dtype = np .uint8 )
142144
143145
144- def braille_encode (
145- a : ArrayLike , # bool[4h, 2w]
146- ) -> np .ndarray : # -> uint16[h, w]
146+ def unicode_braille_array (
147+ dots : ArrayLike , # bool[4h, 2w]
148+ color : Color | None = None ,
149+ ) -> list [list [Char ]]:
147150 """
148151 Turns a HxW array of booleans into a (H//4)x(W//2) array of braille
149152 binary codes.
150153
151154 Inputs:
152155
153- * a : bool[4h, 2w].
156+ * dots : bool[4h, 2w].
154157 Array of booleans, height divisible by 4 and width divisible by 2.
158+ * color: optional Color.
159+ Foreground color used for braille characters.
155160
156161 Returns:
157162
158- * bits: uint16[h, w].
159- An array of braille unicode code points. The unicode characters will
160- have a dot in the corresponding places where `a` is True.
163+ * array: list[list[Char]].
164+ A nested list of Braille characters with H rows and W columns.
161165
162166 An illustrated example is as follows:
163167 ```
@@ -185,26 +189,31 @@ def braille_encode(
185189 convert the braille code to a unicode character and collate into array |
186190 .-----------------------------------------------------------------------'
187191 | '''
188- `->⡇⢸⢸⠉⠁⡇⠀⢸⠀⠀⡎⢱ (Note: this function returns codepoints, use `chr()`
189- ⡏⢹⢸⣉⡁⣇⣀⢸⣀⡀⢇⡸ to convert these into braille characters for printing .)
192+ `->⡇⢸⢸⠉⠁⡇⠀⢸⠀⠀⡎⢱ (Note: this function returns a nested list of Chars
193+ ⡏⢹⢸⣉⡁⣇⣀⢸⣀⡀⢇⡸ rather than a string .)
190194 '''
191195 ```
192196 """
193197 # process input
194- array = np .asarray (a , dtype = bool )
195- H , W = array .shape
198+ dots_ = np .asarray (dots , dtype = bool )
199+ H , W = dots_ .shape
196200 h , w = H // 4 , W // 2
197201
198202 # create a view that chunks it into 4x2 cells
199- cells = array .reshape (h , 4 , w , 2 )
203+ cells = dots_ .reshape (h , 4 , w , 2 )
200204
201205 # convert each bit in each cell into a mask and combine into code array
202206 masks = np .left_shift (cells , BRAILLE_MAP .reshape (1 ,4 ,1 ,2 ), dtype = np .uint16 )
203207 codes = np .bitwise_or .reduce (masks , axis = (1 ,3 ))
204208
205- # unicode braille block starts at 0x2800
206- unicodes = 0x2800 + codes
207- return unicodes
209+ # convert code array into Char array
210+ array = [
211+ [
212+ Char (chr (0x2800 + code ), fg = color ) if code else BLANK
213+ for code in row
214+ ] for row in codes
215+ ]
216+ return array
208217
209218
210219# # #
@@ -323,7 +332,7 @@ def unicode_col(
323332
324333
325334# # #
326- # BOX DRAWING
335+ # UNICODE BOX DRAWING
327336
328337
329338class BoxStyle (str , enum .Enum ):
@@ -360,9 +369,9 @@ class BoxStyle(str, enum.Enum):
360369 ▛──────▜ ▛▀▀▀▀▀▀▜ █▀▀▀▀▀▀█ ▞▝▝▝▝▝▝▝ ▘▘▘▘▘▘▘▚
361370 BLANK │BUMPER│ ▌BLOCK1▐ █BLOCK2█ ▖TIGER1▝ ▘TIGER2▗
362371 ▙──────▟ ▙▄▄▄▄▄▄▟ █▄▄▄▄▄▄█ ▖▖▖▖▖▖▖▞ ▚▗▗▗▗▗▗▗
363- ┬──────┐ ┲━━━━━━┓ ╔══════╗ ╷
364- │LIGHTX│ ┃HEAVYX┃ ║DOUBLX║ │LOWERX
365- ┼──────┤ ╄━━━━━━┩ ╚══════╝ ┼──────╴
372+ ┬──────┐ ┲━━━━━━┓ ╷
373+ │LIGHTX│ ┃HEAVYX┃ │LOWERX
374+ ┼──────┤ ╄━━━━━━┩ ┼──────╴
366375 ```
367376
368377 TODO:
@@ -386,68 +395,124 @@ class BoxStyle(str, enum.Enum):
386395 HEAVYX = "┲━┓┃┃╄━┩"
387396 LOWERX = "╷ │┼─╴"
388397
389-
390398 @property
391- def nw (self ) -> str :
399+ def _nw (self ) -> str :
392400 """Northwest corner symbol."""
393401 return self [0 ]
394-
395402
396403 @property
397- def n (self ) -> str :
404+ def _n (self ) -> str :
398405 """North edge symbol."""
399406 return self [1 ]
400-
401407
402408 @property
403- def ne (self ) -> str :
409+ def _ne (self ) -> str :
404410 """Norteast corner symbol."""
405411 return self [2 ]
406-
407412
408413 @property
409- def e (self ) -> str :
414+ def _e (self ) -> str :
410415 """East edge symbol."""
411416 return self [3 ]
412-
413417
414418 @property
415- def w (self ) -> str :
419+ def _w (self ) -> str :
416420 """West edge symbol."""
417421 return self [4 ]
418-
419422
420423 @property
421- def sw (self ) -> str :
424+ def _sw (self ) -> str :
422425 """Southwest corner symbol."""
423426 return self [5 ]
424-
425427
426428 @property
427- def s (self ) -> str :
429+ def _s (self ) -> str :
428430 """South edge symbol."""
429431 return self [6 ]
430-
431432
432433 @property
433- def se (self ) -> str :
434+ def _se (self ) -> str :
434435 """Southeast corner symbol."""
435436 return self [7 ]
436437
437438
439+ def unicode_box (
440+ array : list [list [Char ]],
441+ style : BoxStyle ,
442+ color : Color | None = None ,
443+ ) -> list [list [Char ]]:
444+ """
445+ Wrap a character array in an outline of box drawing characters.
446+ """
447+ # prepare characters
448+ nw = Char (style ._nw , fg = color )
449+ n = Char (style ._n , fg = color )
450+ ne = Char (style ._ne , fg = color )
451+ w = Char (style ._w , fg = color )
452+ e = Char (style ._e , fg = color )
453+ sw = Char (style ._sw , fg = color )
454+ s = Char (style ._s , fg = color )
455+ se = Char (style ._se , fg = color )
456+ # assemble box
457+ width = len (array [0 ])
458+ array = [
459+ [nw , * [n ] * width , ne ],
460+ * [[w , * row , e ] for row in array ],
461+ [sw , * [s ] * width , se ],
462+ ]
463+ return array
464+
465+
466+ # # #
467+ # UNICODE HALF-BLOCK IMAGE
468+
469+
470+ def unicode_image (
471+ image : NDArray , # u8[h, w, rgb] or float[h, w, rgb]
472+ ) -> list [list [Char ]]:
473+ h , _w , _3 = image .shape
474+
475+ if h % 2 == 1 :
476+ final_row = image [- 1 ]
477+ image = image [:- 1 ]
478+ else :
479+ final_row = None
480+
481+ stacked = einops .rearrange (
482+ image ,
483+ '(h fgbg) w c -> h w fgbg c' ,
484+ fgbg = 2 ,
485+ )
486+ array = [
487+ [
488+ Char (c = "▀" , fg = Color .parse (fg ), bg = Color .parse (bg ))
489+ for fg , bg in row
490+ ]
491+ for row in stacked
492+ ]
493+
494+ if final_row is not None :
495+ array .append ([
496+ Char (c = "▀" , fg = Color .parse (fg ), bg = None )
497+ for fg in final_row
498+ ])
499+
500+ return array
501+
502+
438503# # #
439504# 3D projection
440505
441506
442507def project3 (
443- xyz : np .ndarray , # float[n, 3]
444- camera_position : np .ndarray = np .array ([0. , 0. , 2. ]), # float[3]
445- camera_target : np .ndarray = np .zeros (3 ), # float[3]
446- scene_up : np .ndarray = np .array ([0. ,1. ,0. ]), # float[3]
508+ xyz : np .ndarray , # float[n, 3]
509+ camera_position : np .ndarray = np .array ([0. , 0. , 2. ]), # float[3]
510+ camera_target : np .ndarray = np .zeros (3 ), # float[3]
511+ scene_up : np .ndarray = np .array ([0. ,1. ,0. ]), # float[3]
447512 fov_degrees : float = 90.0 ,
448513) -> tuple [
449- np .ndarray , # float[n, 2]
450- np .ndarray , # bool[n]
514+ np .ndarray , # float[n, 2]
515+ np .ndarray , # bool[n]
451516]:
452517 """
453518 Project a 3d point cloud into two dimensions based on a given camera
0 commit comments