IS-Matrix-Forge 1.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- is_matrix_forge/__init__.py +8 -0
- is_matrix_forge/assets/__init__.py +8 -0
- is_matrix_forge/assets/audio/__init__.py +36 -0
- is_matrix_forge/assets/audio/files/__init__.py +53 -0
- is_matrix_forge/assets/audio/files/plugged.wav +0 -0
- is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
- is_matrix_forge/assets/char_map.json +2150 -0
- is_matrix_forge/assets/font_map.py +119 -0
- is_matrix_forge/common/__init__.py +0 -0
- is_matrix_forge/common/decorators/__init__.py +152 -0
- is_matrix_forge/common/dirs.py +20 -0
- is_matrix_forge/common/helpers/__init__.py +132 -0
- is_matrix_forge/common/helpers/github_api.py +67 -0
- is_matrix_forge/designer_gui/__init__.py +8 -0
- is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
- is_matrix_forge/designer_gui/main_window/layout.py +117 -0
- is_matrix_forge/dev_tools/__init__.py +8 -0
- is_matrix_forge/dev_tools/debug.py +82 -0
- is_matrix_forge/dev_tools/presets.py +47 -0
- is_matrix_forge/inputmodule/__init__.py +164 -0
- is_matrix_forge/inputmodule/b1display.py +160 -0
- is_matrix_forge/inputmodule/c1minimal.py +35 -0
- is_matrix_forge/inputmodule/font.py +2166 -0
- is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
- is_matrix_forge/inputmodule/ledmatrix.py +516 -0
- is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
- is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
- is_matrix_forge/led_matrix/__init__.py +7 -0
- is_matrix_forge/led_matrix/commands/__init__.py +84 -0
- is_matrix_forge/led_matrix/commands/map.py +46 -0
- is_matrix_forge/led_matrix/constants.py +82 -0
- is_matrix_forge/led_matrix/controller/__init__.py +26 -0
- is_matrix_forge/led_matrix/controller/controller.py +635 -0
- is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
- is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
- is_matrix_forge/led_matrix/controller/multiton.py +32 -0
- is_matrix_forge/led_matrix/controller/threaded.py +72 -0
- is_matrix_forge/led_matrix/display/__init__.py +13 -0
- is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
- is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
- is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
- is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
- is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
- is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
- is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
- is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
- is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
- is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
- is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
- is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
- is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
- is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
- is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
- is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
- is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
- is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
- is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
- is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
- is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
- is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
- is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
- is_matrix_forge/led_matrix/display/media.py +199 -0
- is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
- is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
- is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
- is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
- is_matrix_forge/led_matrix/errors/__init__.py +4 -0
- is_matrix_forge/led_matrix/errors/base.py +14 -0
- is_matrix_forge/led_matrix/errors/grid.py +37 -0
- is_matrix_forge/led_matrix/errors/matrix.py +68 -0
- is_matrix_forge/led_matrix/errors/misc.py +36 -0
- is_matrix_forge/led_matrix/fonts.py +20 -0
- is_matrix_forge/led_matrix/hardware.py +172 -0
- is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
- is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
- is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
- is_matrix_forge/led_matrix/helpers/device.py +118 -0
- is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
- is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
- is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
- is_matrix_forge/led_matrix/led_matrix.py +25 -0
- is_matrix_forge/led_matrix/status.py +8 -0
- is_matrix_forge/led_matrix/tools/__init__.py +8 -0
- is_matrix_forge/led_matrix/tools/identify.py +27 -0
- is_matrix_forge/led_matrix/utils/__init__.py +41 -0
- is_matrix_forge/log_engine.py +25 -0
- is_matrix_forge/monitor/__init__.py +68 -0
- is_matrix_forge/monitor/errors.py +35 -0
- is_matrix_forge/monitor/events.py +78 -0
- is_matrix_forge/monitor/gui/__init__.py +0 -0
- is_matrix_forge/monitor/gui/errors.py +35 -0
- is_matrix_forge/monitor/gui/event/__init__.py +8 -0
- is_matrix_forge/monitor/gui/event/collection.py +153 -0
- is_matrix_forge/monitor/gui/event/errors.py +58 -0
- is_matrix_forge/monitor/gui/event/event.py +226 -0
- is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
- is_matrix_forge/monitor/gui/layout/base.py +147 -0
- is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
- is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
- is_matrix_forge/monitor/gui/windows/base.py +325 -0
- is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
- is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
- is_matrix_forge/monitor/helpers.py +55 -0
- is_matrix_forge/monitor/monitor.py +431 -0
- is_matrix_forge/notify/__init__.py +0 -0
- is_matrix_forge/notify/sounds/__init__.py +6 -0
- is_matrix_forge/notify/sounds/base.py +106 -0
- is_matrix_forge/progress.py +99 -0
- is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
- is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
- is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
- is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
- is_matrix_forge-1.0.0.dev1.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# font_map_builder.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Author: Taylor
|
|
5
|
+
Project: Inspyre-Toolbox (LED-Matrix Extensions)
|
|
6
|
+
File: font_map_builder.py
|
|
7
|
+
|
|
8
|
+
Description:
|
|
9
|
+
Build a mapping from characters to 2D binary grids
|
|
10
|
+
suitable for driving your TextScroller.
|
|
11
|
+
|
|
12
|
+
Dependencies:
|
|
13
|
+
- pillow (PIL)
|
|
14
|
+
|
|
15
|
+
Classes:
|
|
16
|
+
FontMapBuilderConfig
|
|
17
|
+
FontMapBuilder
|
|
18
|
+
|
|
19
|
+
Example Usage:
|
|
20
|
+
from font_map_builder import FontMapBuilder, FontMapBuilderConfig
|
|
21
|
+
|
|
22
|
+
config = FontMapBuilderConfig(
|
|
23
|
+
font_path="C:/Windows/Fonts/arial.ttf",
|
|
24
|
+
font_size=8,
|
|
25
|
+
canvas_size=(5, 7), # width x height in pixels
|
|
26
|
+
characters="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?,.",
|
|
27
|
+
threshold=128,
|
|
28
|
+
)
|
|
29
|
+
builder = FontMapBuilder(config)
|
|
30
|
+
font_map = builder.generate_font_map()
|
|
31
|
+
# font_map["A"] is now a 7-row x 5-col list of 0/1s
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import Dict, List, Tuple
|
|
36
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class FontMapBuilderConfig:
|
|
41
|
+
"""Immutable settings for font‐to‐grid conversion."""
|
|
42
|
+
font_path: str
|
|
43
|
+
font_size: int
|
|
44
|
+
canvas_size: Tuple[int, int]
|
|
45
|
+
characters: str
|
|
46
|
+
threshold: int = 128 # pixel brightness cutoff (0–255)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FontMapBuilder:
|
|
50
|
+
"""Generates a font_map mapping each character to a binary grid."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: FontMapBuilderConfig):
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
config: All settings needed to render and threshold glyphs.
|
|
56
|
+
"""
|
|
57
|
+
self.config = config
|
|
58
|
+
self._font = ImageFont.truetype(config.font_path, config.font_size)
|
|
59
|
+
|
|
60
|
+
def generate_font_map(self) -> Dict[str, List[List[int]]]:
|
|
61
|
+
"""
|
|
62
|
+
Render every character in `config.characters` into a
|
|
63
|
+
monochrome grid, then threshold to 0/1.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A dict mapping each character to its grid (rows of ints).
|
|
67
|
+
"""
|
|
68
|
+
font_map: Dict[str, List[List[int]]] = {}
|
|
69
|
+
w, h = self.config.canvas_size
|
|
70
|
+
|
|
71
|
+
for char in self.config.characters:
|
|
72
|
+
if char in self.config.characters:
|
|
73
|
+
if char == ' ':
|
|
74
|
+
grid = [[0] * w for _ in range(h)]
|
|
75
|
+
else:
|
|
76
|
+
img = self._render_char(char, w, h)
|
|
77
|
+
grid = self._image_to_grid(img)
|
|
78
|
+
font_map[char] = grid
|
|
79
|
+
|
|
80
|
+
if ' ' not in font_map:
|
|
81
|
+
font_map[' '] = [[0] * w for _ in range(h)]
|
|
82
|
+
|
|
83
|
+
return font_map
|
|
84
|
+
|
|
85
|
+
def _render_char(self, ch: str, width: int, height: int) -> Image.Image:
|
|
86
|
+
"""
|
|
87
|
+
Draw a single character centered in a WxH grayscale image.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A Pillow Image in mode 'L'.
|
|
91
|
+
"""
|
|
92
|
+
img = Image.new("L", (width, height), color=0)
|
|
93
|
+
draw = ImageDraw.Draw(img)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
left, top, right, bottom = draw.textbbox((0, 0), ch, font=self._font)
|
|
97
|
+
text_w, text_h = right - left, bottom - top
|
|
98
|
+
x = (width - text_w) // 2 - left
|
|
99
|
+
y = (height - text_h) // 2 - top
|
|
100
|
+
except AttributeError as e:
|
|
101
|
+
# Maintain compatibility with pre 8.0.0 Pillow
|
|
102
|
+
text_w, text_h = self._font.getsize(ch)
|
|
103
|
+
x = (width - text_w) // 2
|
|
104
|
+
y = (height - text_h) // 2
|
|
105
|
+
|
|
106
|
+
draw.text((x, y), ch, fill=255, font=self._font)
|
|
107
|
+
return img
|
|
108
|
+
|
|
109
|
+
def _image_to_grid(self, img: Image.Image) -> List[List[int]]:
|
|
110
|
+
"""
|
|
111
|
+
Convert a grayscale image to a 2D list of 0/1
|
|
112
|
+
based on the luminosity threshold.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of rows, each a list of ints (0 or 1).
|
|
116
|
+
"""
|
|
117
|
+
pixels = img.load()
|
|
118
|
+
w, h = img.size
|
|
119
|
+
thresh = self.config.threshold
|
|
120
|
+
return [
|
|
121
|
+
[1 if pixels[x, y] >= thresh else 0 for x in range(w)]
|
|
122
|
+
for y in range(h)
|
|
123
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from .base import Frame
|
|
3
|
+
from serial.tools.list_ports_common import ListPortInfo
|
|
4
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
|
|
5
|
+
from is_matrix_forge.led_matrix.display.animations.errors import MalformedGridError
|
|
6
|
+
from is_matrix_forge.led_matrix import render_matrix
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
running = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def hold_pattern(dev, grid: List[List[int]], reapply_interval: float = 55.00) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Hold the state of the LED matrix indefinitely, only updating the display every `reapply_interval` seconds.
|
|
15
|
+
|
|
16
|
+
This is useful for maintaining a static display on the LED matrix without having a constant refresh/computation
|
|
17
|
+
load.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
dev (ListPortInfo):
|
|
21
|
+
The serial com device/port to use.
|
|
22
|
+
|
|
23
|
+
grid (List[List[int]])::
|
|
24
|
+
The grid to display on the LED matrix.
|
|
25
|
+
|
|
26
|
+
reapply_interval (Union[float, int]):
|
|
27
|
+
The interval in seconds at which to reapply the grid state to the LED matrix. Default is 55 seconds,
|
|
28
|
+
maximum is 60 (the number of seconds before the matrix turns all the LEDs off unless a new state is
|
|
29
|
+
applied)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
None
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if not isinstance(dev, ListPortInfo):
|
|
36
|
+
raise TypeError(f"dev must be a ListPortInfo object, not {type(dev)}")
|
|
37
|
+
|
|
38
|
+
if not isinstance(grid, list) or not is_valid_grid(grid, 9, 34):
|
|
39
|
+
raise MalformedGridError(f"grid must be a 9x34 list of 0/1, not {type(grid)}")
|
|
40
|
+
|
|
41
|
+
cycles = 0
|
|
42
|
+
def worker(interval):
|
|
43
|
+
global running, cycles
|
|
44
|
+
running = True
|
|
45
|
+
while running:
|
|
46
|
+
cycles += 1
|
|
47
|
+
render_matrix(dev, grid)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# is_matrix_forge.led_matrix.display.animations.frame.base
|
|
2
|
+
# Standard library imports
|
|
3
|
+
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from typing import List, Any, Union
|
|
7
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
|
|
8
|
+
|
|
9
|
+
from is_matrix_forge.led_matrix import render_matrix
|
|
10
|
+
from is_matrix_forge.led_matrix.display.grid.grid import Grid
|
|
11
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import generate_blank_grid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Frame:
|
|
15
|
+
"""
|
|
16
|
+
Represents a single animation frame with a duration.
|
|
17
|
+
"""
|
|
18
|
+
DEFAULT_DURATION = 0.33
|
|
19
|
+
|
|
20
|
+
def __init__(self, grid: Union[List[List[int]], Grid], duration: float = 1.0):
|
|
21
|
+
self.__grid = None
|
|
22
|
+
self.__number_of_plays = 0
|
|
23
|
+
self.__duration = duration
|
|
24
|
+
|
|
25
|
+
# Accept Grid instances by extracting their underlying data
|
|
26
|
+
if grid is None:
|
|
27
|
+
grid = generate_blank_grid()
|
|
28
|
+
elif isinstance(grid, Grid):
|
|
29
|
+
# Grid stores column-major lists in _grid
|
|
30
|
+
grid = getattr(grid, '_grid', list(grid))
|
|
31
|
+
|
|
32
|
+
# Determine dimensions from column-major layout
|
|
33
|
+
self.__width = len(grid)
|
|
34
|
+
self.__height = len(grid[0])
|
|
35
|
+
# Validate and store grid data
|
|
36
|
+
self.grid = grid
|
|
37
|
+
|
|
38
|
+
if duration is not None:
|
|
39
|
+
self.duration = duration
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def duration(self) -> float:
|
|
43
|
+
"""
|
|
44
|
+
The time the program should wait after displaying this frame before moving to the next.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
float: Frame duration in seconds.
|
|
48
|
+
"""
|
|
49
|
+
return self.__duration or self.DEFAULT_DURATION
|
|
50
|
+
|
|
51
|
+
@duration.setter
|
|
52
|
+
def duration(self, new: Union[float, int, str]):
|
|
53
|
+
"""
|
|
54
|
+
Set the duration of the frame in seconds.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
new (Union[float, int, str]):
|
|
58
|
+
The duration of the frame in seconds, or fraction thereof, or a string.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
dur = float(new)
|
|
62
|
+
except (TypeError, ValueError) as e:
|
|
63
|
+
raise ValueError(f"Invalid duration: {new}") from e
|
|
64
|
+
if dur < 0:
|
|
65
|
+
raise ValueError(f"Duration must be positive, not {dur}")
|
|
66
|
+
self.__duration = dur
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def grid(self) -> List[List[int]]:
|
|
70
|
+
return self.__grid
|
|
71
|
+
|
|
72
|
+
@grid.setter
|
|
73
|
+
def grid(self, value: Any) -> None:
|
|
74
|
+
# If user passed a Grid, unwrap to list-of-columns
|
|
75
|
+
#if isinstance(value, Grid):
|
|
76
|
+
# value = getattr(value, '_grid', list(value))
|
|
77
|
+
# if not is_valid_grid(value, self.__width, self.__height):
|
|
78
|
+
# raise ValueError(f"Grid must be {self.__width}x{self.__height} list of 0/1")
|
|
79
|
+
self.__grid = value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def number_of_plays(self) -> int:
|
|
83
|
+
return self.__number_of_plays
|
|
84
|
+
|
|
85
|
+
@number_of_plays.setter
|
|
86
|
+
def number_of_plays(self, new: int) -> None:
|
|
87
|
+
if not isinstance(new, int) or new < 0:
|
|
88
|
+
raise ValueError("Number of plays must be a non-negative integer")
|
|
89
|
+
self.__number_of_plays = new
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
return f"Frame(grid={self.grid}, duration={self.duration})"
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def from_dict(data: dict) -> 'Frame':
|
|
96
|
+
"""
|
|
97
|
+
Create a `Frame` object directly from a properly formatted dictionary.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
data (dict): A dictionary containing 'grid' and optional 'duration'.
|
|
101
|
+
Returns:
|
|
102
|
+
Frame: A new `Frame` object.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
return Frame(
|
|
106
|
+
grid=data['grid'],
|
|
107
|
+
duration=data.get('duration', Frame.DEFAULT_DURATION)
|
|
108
|
+
)
|
|
109
|
+
except KeyError as e:
|
|
110
|
+
raise e from e
|
|
111
|
+
|
|
112
|
+
def play(self, device) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Play the frame on the LED matrix device.
|
|
115
|
+
"""
|
|
116
|
+
device.draw_grid(self.grid)
|
|
117
|
+
sleep(self.duration)
|
|
118
|
+
self.__number_of_plays += 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# class Frame:
|
|
123
|
+
# """
|
|
124
|
+
# Represents a single animation frame with a duration.
|
|
125
|
+
# """
|
|
126
|
+
# DEFAULT_DURATION = 0.33
|
|
127
|
+
#
|
|
128
|
+
# def __init__(self, grid: List[List[int]], duration: float = 1.0):
|
|
129
|
+
# self.__grid = None
|
|
130
|
+
# self.__number_of_plays = 0
|
|
131
|
+
# self.__duration = duration
|
|
132
|
+
#
|
|
133
|
+
# if grid is None:
|
|
134
|
+
# grid = generate_blank_grid()
|
|
135
|
+
#
|
|
136
|
+
# self.__width = len(grid)
|
|
137
|
+
# self.__height = len(grid[0])
|
|
138
|
+
# self.grid = grid
|
|
139
|
+
#
|
|
140
|
+
# if duration is not None:
|
|
141
|
+
# self.duration = duration
|
|
142
|
+
#
|
|
143
|
+
# @property
|
|
144
|
+
# def duration(self) -> float:
|
|
145
|
+
# """
|
|
146
|
+
# The time the program should wait after displaying this frame before moving to the next.
|
|
147
|
+
#
|
|
148
|
+
# Parameters:
|
|
149
|
+
#
|
|
150
|
+
# """
|
|
151
|
+
#
|
|
152
|
+
# return self.__duration or self.DEFAULT_DURATION
|
|
153
|
+
#
|
|
154
|
+
# @duration.setter
|
|
155
|
+
# def duration(self, new: Union[float, int, str]):
|
|
156
|
+
# """
|
|
157
|
+
# Set the duration of the frame in seconds.
|
|
158
|
+
#
|
|
159
|
+
# Parameters:
|
|
160
|
+
# new (Union[float, int, str]):
|
|
161
|
+
# The duration of the frame in seconds, or fractions thereof, or a string representing a fraction of a
|
|
162
|
+
# second/full-second.
|
|
163
|
+
# """
|
|
164
|
+
# try:
|
|
165
|
+
# dur = float(new)
|
|
166
|
+
# except (TypeError, ValueError) as e:
|
|
167
|
+
# raise ValueError(f"Invalid duration: {new}") from e
|
|
168
|
+
# if dur < 0:
|
|
169
|
+
# raise ValueError(f"Duration must be positive, not {dur}")
|
|
170
|
+
# self.__duration = dur
|
|
171
|
+
#
|
|
172
|
+
# @property
|
|
173
|
+
# def grid(self) -> List[List[int]]:
|
|
174
|
+
# return self.__grid
|
|
175
|
+
#
|
|
176
|
+
# @grid.setter
|
|
177
|
+
# def grid(self, value: Any) -> None:
|
|
178
|
+
# if not is_valid_grid(value, self.__width, self.__height):
|
|
179
|
+
# raise ValueError(f"Grid must be {self.__width}x{self.__height} list of 0/1")
|
|
180
|
+
# self.__grid = value
|
|
181
|
+
#
|
|
182
|
+
# @property
|
|
183
|
+
# def number_of_plays(self):
|
|
184
|
+
# return self.__number_of_plays
|
|
185
|
+
#
|
|
186
|
+
# @number_of_plays.setter
|
|
187
|
+
# def number_of_plays(self, new: int) -> None:
|
|
188
|
+
# if not isinstance(new, int) or new < 0:
|
|
189
|
+
# raise ValueError("Number of plays must be a non-negative integer")
|
|
190
|
+
# self.__number_of_plays = new
|
|
191
|
+
#
|
|
192
|
+
# def __repr__(self) -> str:
|
|
193
|
+
# return f"Frame(grid={self.grid}, duration={self.duration})"
|
|
194
|
+
#
|
|
195
|
+
# # width/height context for validation inherited from usage
|
|
196
|
+
# @property
|
|
197
|
+
# def width(self) -> int:
|
|
198
|
+
# return self.__width
|
|
199
|
+
#
|
|
200
|
+
# @property
|
|
201
|
+
# def height(self) -> int:
|
|
202
|
+
# return self.__height
|
|
203
|
+
#
|
|
204
|
+
# @staticmethod
|
|
205
|
+
# def from_dict(data: dict) -> 'Frame':
|
|
206
|
+
# """
|
|
207
|
+
# Create a `Frame` object directly from a properly formatted dictionary.
|
|
208
|
+
#
|
|
209
|
+
# The dictionary must contain the following keys:
|
|
210
|
+
# - `grid`:
|
|
211
|
+
# A list of lists representing the grid of the frame.
|
|
212
|
+
#
|
|
213
|
+
# - `duration`:
|
|
214
|
+
# The duration of the frame in seconds.
|
|
215
|
+
#
|
|
216
|
+
# Parameters:
|
|
217
|
+
# data (dict):
|
|
218
|
+
# A dictionary containing the data for the frame.
|
|
219
|
+
#
|
|
220
|
+
# Returns:
|
|
221
|
+
# Frame:
|
|
222
|
+
# A new `Frame` object.
|
|
223
|
+
# """
|
|
224
|
+
# try:
|
|
225
|
+
# return Frame(
|
|
226
|
+
# grid=data['grid'],
|
|
227
|
+
# duration=data['duration']
|
|
228
|
+
# )
|
|
229
|
+
# except KeyError as e:
|
|
230
|
+
# key = e.args[0]
|
|
231
|
+
# if key == 'duration':
|
|
232
|
+
# return Frame(
|
|
233
|
+
# grid=data['grid'],
|
|
234
|
+
# duration=Frame.DEFAULT_DURATION
|
|
235
|
+
# )
|
|
236
|
+
# else:
|
|
237
|
+
# raise e from e
|
|
238
|
+
#
|
|
239
|
+
# def play(self, device) -> None:
|
|
240
|
+
# """
|
|
241
|
+
# Play the frame on the LED matrix.
|
|
242
|
+
# """
|
|
243
|
+
# render_matrix(device, self.grid)
|
|
244
|
+
# sleep(self.duration)
|
|
245
|
+
# self.__number_of_plays += 1
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Any, List, Union
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from inspyre_toolbox.path_man import provision_path
|
|
5
|
+
|
|
6
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_path(path: Union[str, Path], skip_exists_check: bool = False) -> Path:
|
|
10
|
+
path = provision_path(path)
|
|
11
|
+
|
|
12
|
+
if not skip_exists_check and not path.exists():
|
|
13
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
14
|
+
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_frames_from_file(path: Union[str, Path]) -> List['Frame']:
|
|
19
|
+
from is_matrix_forge.led_matrix.display.animations.frame.base import Frame
|
|
20
|
+
from is_matrix_forge.led_matrix.helpers import get_json_from_file
|
|
21
|
+
|
|
22
|
+
path = provision_path(path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_valid_frames(raw: Any, width: int, height: int) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Check if a list of frames is valid.
|
|
29
|
+
|
|
30
|
+
A list of frames is valid if it is a list of lists of 0s and 1s, and the length of each list is equal to the width
|
|
31
|
+
and height.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
raw (Any):
|
|
36
|
+
The list of frames to check.
|
|
37
|
+
|
|
38
|
+
width (int):
|
|
39
|
+
The expected width (in pixels) of the frames.
|
|
40
|
+
|
|
41
|
+
height (int):
|
|
42
|
+
The expected height (in pixels) of the frames.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool:
|
|
46
|
+
- True if the list of frames is valid.
|
|
47
|
+
- False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
return (
|
|
50
|
+
isinstance(raw, list)
|
|
51
|
+
and len(raw) > 0
|
|
52
|
+
and all(is_valid_grid(frame, width, height) for frame in raw)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def migrate_frame(matrix_spec: List[List[int]], duration: Union[int, str]) -> dict:
|
|
57
|
+
"""
|
|
58
|
+
Creates a frame entry from a matrix spec.
|
|
59
|
+
|
|
60
|
+
Note:
|
|
61
|
+
A 'matrix spec' is a list of lists each containing 0s and 1s representing the pixels of a frame. 1 indicates a
|
|
62
|
+
pixel is on; 0 indicates it is off.
|
|
63
|
+
"""
|
|
64
|
+
return {
|
|
65
|
+
'grid': matrix_spec,
|
|
66
|
+
'duration': duration
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from threading import Thread
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from is_matrix_forge.led_matrix.display.grid.grid import MATRIX_WIDTH, MATRIX_HEIGHT
|
|
5
|
+
from is_matrix_forge.led_matrix.display.grid import Grid
|
|
6
|
+
from is_matrix_forge.led_matrix.display.animations.animation import Animation, Frame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class TextScrollerConfig:
|
|
11
|
+
"""Immutable configuration for the text scroller."""
|
|
12
|
+
text: str
|
|
13
|
+
font_map: Dict[str, List[List[int]]]
|
|
14
|
+
spacing: int = 1
|
|
15
|
+
frame_duration: float = 0.05
|
|
16
|
+
wrap: bool = False # unused for vertical mode
|
|
17
|
+
direction: str = "horizontal" # one of: "horizontal", "vertical_up", "vertical_down"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TextScroller:
|
|
21
|
+
"""Generates scrolling‐text Animations in horizontal or vertical directions."""
|
|
22
|
+
|
|
23
|
+
VALID_DIRS = {"horizontal", "vertical_up", "vertical_down"}
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: TextScrollerConfig):
|
|
26
|
+
self.config = config
|
|
27
|
+
if config.direction not in self.VALID_DIRS:
|
|
28
|
+
raise ValueError(f"Unsupported scroll direction: {config.direction}")
|
|
29
|
+
# ensure font fits display height
|
|
30
|
+
sample = next(iter(config.font_map.values()))
|
|
31
|
+
if len(sample) > MATRIX_HEIGHT:
|
|
32
|
+
raise ValueError(f"Font height {len(sample)} > display height {MATRIX_HEIGHT}")
|
|
33
|
+
|
|
34
|
+
def generate_animation(self) -> Animation:
|
|
35
|
+
"""Build and return your scrolling‐text Animation.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: if `text` contains a character not in `font_map`.
|
|
39
|
+
"""
|
|
40
|
+
# build list of glyphs from text
|
|
41
|
+
glyphs: List[List[List[int]]] = [] # each glyph: rows of cols
|
|
42
|
+
for ch in self.config.text:
|
|
43
|
+
glyph = self.config.font_map.get(ch)
|
|
44
|
+
if glyph is None:
|
|
45
|
+
raise ValueError(f"Character '{ch}' not found in font_map")
|
|
46
|
+
glyphs.append(glyph)
|
|
47
|
+
|
|
48
|
+
glyph_h = len(glyphs[0])
|
|
49
|
+
glyph_ws = [len(g[0]) for g in glyphs]
|
|
50
|
+
|
|
51
|
+
frames: List[Frame] = []
|
|
52
|
+
|
|
53
|
+
if self.config.direction == "horizontal":
|
|
54
|
+
# existing horizontal behavior unchanged
|
|
55
|
+
total_w = sum(glyph_ws) + self.config.spacing * (len(glyph_ws) - 1)
|
|
56
|
+
canvas = [[0] * total_w for _ in range(MATRIX_HEIGHT)]
|
|
57
|
+
x = 0
|
|
58
|
+
# render glyphs horizontally
|
|
59
|
+
for g, w in zip(glyphs, glyph_ws):
|
|
60
|
+
vpad = (MATRIX_HEIGHT - glyph_h) // 2
|
|
61
|
+
for r in range(glyph_h):
|
|
62
|
+
canvas[vpad + r][x : x + w] = g[r]
|
|
63
|
+
x += w + self.config.spacing
|
|
64
|
+
|
|
65
|
+
offsets = range(0, total_w) if self.config.wrap else range(-MATRIX_WIDTH, total_w)
|
|
66
|
+
for off in offsets:
|
|
67
|
+
window = [
|
|
68
|
+
[canvas[r][off + c] if 0 <= off + c < total_w else 0 for c in range(MATRIX_WIDTH)]
|
|
69
|
+
for r in range(MATRIX_HEIGHT)
|
|
70
|
+
]
|
|
71
|
+
cols = [[window[r][c] for r in range(MATRIX_HEIGHT)] for c in range(MATRIX_WIDTH)]
|
|
72
|
+
frames.append(Frame(grid=Grid(init_grid=cols)))
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
# vertical mode: scroll each glyph in turn, center-justified
|
|
76
|
+
direction = self.config.direction
|
|
77
|
+
for glyph, w in zip(glyphs, glyph_ws):
|
|
78
|
+
# horizontal center offset
|
|
79
|
+
x0 = (MATRIX_WIDTH - w) // 2
|
|
80
|
+
# choose offset range
|
|
81
|
+
if direction == "vertical_up":
|
|
82
|
+
offsets = range(MATRIX_HEIGHT, -glyph_h - 1, -1)
|
|
83
|
+
else: # vertical_down
|
|
84
|
+
offsets = range(-glyph_h, MATRIX_HEIGHT + 1)
|
|
85
|
+
# scroll this glyph
|
|
86
|
+
for off in offsets:
|
|
87
|
+
window = [[0] * MATRIX_WIDTH for _ in range(MATRIX_HEIGHT)]
|
|
88
|
+
for r in range(glyph_h):
|
|
89
|
+
for c in range(w):
|
|
90
|
+
if glyph[r][c]:
|
|
91
|
+
y = off + r
|
|
92
|
+
if 0 <= y < MATRIX_HEIGHT:
|
|
93
|
+
window[y][x0 + c] = 1
|
|
94
|
+
cols = [[window[r][c] for r in range(MATRIX_HEIGHT)] for c in range(MATRIX_WIDTH)]
|
|
95
|
+
frames.append(Frame(grid=Grid(init_grid=cols)))
|
|
96
|
+
# blank-gap frames between letters
|
|
97
|
+
for _ in range(self.config.spacing):
|
|
98
|
+
blank = [[0] * MATRIX_WIDTH for _ in range(MATRIX_HEIGHT)]
|
|
99
|
+
cols = [[blank[r][c] for r in range(MATRIX_HEIGHT)] for c in range(MATRIX_WIDTH)]
|
|
100
|
+
frames.append(Frame(grid=Grid(init_grid=cols)))
|
|
101
|
+
|
|
102
|
+
anim = Animation(frame_data=frames)
|
|
103
|
+
anim.set_all_frame_durations(self.config.frame_duration)
|
|
104
|
+
return anim
|