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.
Files changed (123) hide show
  1. is_matrix_forge/__init__.py +8 -0
  2. is_matrix_forge/assets/__init__.py +8 -0
  3. is_matrix_forge/assets/audio/__init__.py +36 -0
  4. is_matrix_forge/assets/audio/files/__init__.py +53 -0
  5. is_matrix_forge/assets/audio/files/plugged.wav +0 -0
  6. is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
  7. is_matrix_forge/assets/char_map.json +2150 -0
  8. is_matrix_forge/assets/font_map.py +119 -0
  9. is_matrix_forge/common/__init__.py +0 -0
  10. is_matrix_forge/common/decorators/__init__.py +152 -0
  11. is_matrix_forge/common/dirs.py +20 -0
  12. is_matrix_forge/common/helpers/__init__.py +132 -0
  13. is_matrix_forge/common/helpers/github_api.py +67 -0
  14. is_matrix_forge/designer_gui/__init__.py +8 -0
  15. is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
  16. is_matrix_forge/designer_gui/main_window/layout.py +117 -0
  17. is_matrix_forge/dev_tools/__init__.py +8 -0
  18. is_matrix_forge/dev_tools/debug.py +82 -0
  19. is_matrix_forge/dev_tools/presets.py +47 -0
  20. is_matrix_forge/inputmodule/__init__.py +164 -0
  21. is_matrix_forge/inputmodule/b1display.py +160 -0
  22. is_matrix_forge/inputmodule/c1minimal.py +35 -0
  23. is_matrix_forge/inputmodule/font.py +2166 -0
  24. is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
  25. is_matrix_forge/inputmodule/ledmatrix.py +516 -0
  26. is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
  27. is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
  28. is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
  29. is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
  30. is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
  31. is_matrix_forge/led_matrix/__init__.py +7 -0
  32. is_matrix_forge/led_matrix/commands/__init__.py +84 -0
  33. is_matrix_forge/led_matrix/commands/map.py +46 -0
  34. is_matrix_forge/led_matrix/constants.py +82 -0
  35. is_matrix_forge/led_matrix/controller/__init__.py +26 -0
  36. is_matrix_forge/led_matrix/controller/controller.py +635 -0
  37. is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
  38. is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
  39. is_matrix_forge/led_matrix/controller/multiton.py +32 -0
  40. is_matrix_forge/led_matrix/controller/threaded.py +72 -0
  41. is_matrix_forge/led_matrix/display/__init__.py +13 -0
  42. is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
  43. is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
  44. is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
  45. is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
  46. is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
  47. is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
  48. is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
  49. is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
  50. is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
  51. is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
  52. is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
  53. is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
  54. is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
  55. is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
  56. is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
  57. is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
  58. is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
  59. is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
  60. is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
  61. is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
  62. is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
  63. is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
  64. is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
  65. is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
  66. is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
  67. is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
  68. is_matrix_forge/led_matrix/display/media.py +199 -0
  69. is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
  70. is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
  71. is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
  72. is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
  73. is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
  74. is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
  75. is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
  76. is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
  77. is_matrix_forge/led_matrix/errors/__init__.py +4 -0
  78. is_matrix_forge/led_matrix/errors/base.py +14 -0
  79. is_matrix_forge/led_matrix/errors/grid.py +37 -0
  80. is_matrix_forge/led_matrix/errors/matrix.py +68 -0
  81. is_matrix_forge/led_matrix/errors/misc.py +36 -0
  82. is_matrix_forge/led_matrix/fonts.py +20 -0
  83. is_matrix_forge/led_matrix/hardware.py +172 -0
  84. is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
  85. is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
  86. is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
  87. is_matrix_forge/led_matrix/helpers/device.py +118 -0
  88. is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
  89. is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
  90. is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
  91. is_matrix_forge/led_matrix/led_matrix.py +25 -0
  92. is_matrix_forge/led_matrix/status.py +8 -0
  93. is_matrix_forge/led_matrix/tools/__init__.py +8 -0
  94. is_matrix_forge/led_matrix/tools/identify.py +27 -0
  95. is_matrix_forge/led_matrix/utils/__init__.py +41 -0
  96. is_matrix_forge/log_engine.py +25 -0
  97. is_matrix_forge/monitor/__init__.py +68 -0
  98. is_matrix_forge/monitor/errors.py +35 -0
  99. is_matrix_forge/monitor/events.py +78 -0
  100. is_matrix_forge/monitor/gui/__init__.py +0 -0
  101. is_matrix_forge/monitor/gui/errors.py +35 -0
  102. is_matrix_forge/monitor/gui/event/__init__.py +8 -0
  103. is_matrix_forge/monitor/gui/event/collection.py +153 -0
  104. is_matrix_forge/monitor/gui/event/errors.py +58 -0
  105. is_matrix_forge/monitor/gui/event/event.py +226 -0
  106. is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
  107. is_matrix_forge/monitor/gui/layout/base.py +147 -0
  108. is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
  109. is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
  110. is_matrix_forge/monitor/gui/windows/base.py +325 -0
  111. is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
  112. is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
  113. is_matrix_forge/monitor/helpers.py +55 -0
  114. is_matrix_forge/monitor/monitor.py +431 -0
  115. is_matrix_forge/notify/__init__.py +0 -0
  116. is_matrix_forge/notify/sounds/__init__.py +6 -0
  117. is_matrix_forge/notify/sounds/base.py +106 -0
  118. is_matrix_forge/progress.py +99 -0
  119. is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
  120. is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
  121. is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
  122. is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
  123. 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
@@ -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
@@ -0,0 +1,17 @@
1
+ """
2
+
3
+
4
+ Author:
5
+ Inspyre Softworks
6
+
7
+ Project:
8
+ led-matrix-battery
9
+
10
+ File:
11
+ is_matrix_forge/led_matrix/display/effects/__init__.py
12
+
13
+
14
+ Description:
15
+
16
+
17
+ """