scrollkit 0.8.3__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.
- scrollkit/__init__.py +16 -0
- scrollkit/app/__init__.py +6 -0
- scrollkit/app/base.py +918 -0
- scrollkit/app/memory.py +70 -0
- scrollkit/config/__init__.py +1 -0
- scrollkit/config/settings_manager.py +215 -0
- scrollkit/config/transition_names.py +31 -0
- scrollkit/dev/__init__.py +41 -0
- scrollkit/dev/capabilities.py +374 -0
- scrollkit/dev/harness.py +383 -0
- scrollkit/dev/metrics.py +91 -0
- scrollkit/dev/performance.py +174 -0
- scrollkit/dev/validation.py +245 -0
- scrollkit/display/__init__.py +14 -0
- scrollkit/display/_graphics.py +299 -0
- scrollkit/display/_recording.py +165 -0
- scrollkit/display/_sim_backend.py +99 -0
- scrollkit/display/bitmap_text.py +408 -0
- scrollkit/display/boards.py +186 -0
- scrollkit/display/colors.py +176 -0
- scrollkit/display/content.py +604 -0
- scrollkit/display/gradient_text.py +167 -0
- scrollkit/display/interface.py +126 -0
- scrollkit/display/simulator.py +80 -0
- scrollkit/display/text_fill.py +59 -0
- scrollkit/display/text_pixels.py +250 -0
- scrollkit/display/unified.py +595 -0
- scrollkit/effects/__init__.py +28 -0
- scrollkit/effects/drip_splash.py +253 -0
- scrollkit/effects/easing.py +131 -0
- scrollkit/effects/overlay.py +92 -0
- scrollkit/effects/particles.py +355 -0
- scrollkit/effects/reveal_splash.py +132 -0
- scrollkit/effects/scrolling.py +363 -0
- scrollkit/effects/swarm_reveal.py +512 -0
- scrollkit/effects/text_render.py +25 -0
- scrollkit/effects/transitions.py +873 -0
- scrollkit/exceptions.py +55 -0
- scrollkit/network/__init__.py +1 -0
- scrollkit/network/http_client.py +505 -0
- scrollkit/network/mdns.py +44 -0
- scrollkit/network/wifi_manager.py +382 -0
- scrollkit/ota/__init__.py +13 -0
- scrollkit/ota/client.py +528 -0
- scrollkit/ota/display_progress.py +125 -0
- scrollkit/ota/manifest.py +206 -0
- scrollkit/ota/publish.py +379 -0
- scrollkit/simulator/ATTRIBUTION.md +20 -0
- scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
- scrollkit/simulator/LICENSE +176 -0
- scrollkit/simulator/README.md +87 -0
- scrollkit/simulator/__init__.py +11 -0
- scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
- scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
- scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
- scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
- scrollkit/simulator/adafruit_display_text/label.py +336 -0
- scrollkit/simulator/bitmaptools.py +50 -0
- scrollkit/simulator/core/__init__.py +8 -0
- scrollkit/simulator/core/color_utils.py +62 -0
- scrollkit/simulator/core/device_benchmarks.json +254 -0
- scrollkit/simulator/core/feasibility.py +160 -0
- scrollkit/simulator/core/hardware_profile.py +191 -0
- scrollkit/simulator/core/led_matrix.py +307 -0
- scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
- scrollkit/simulator/core/performance_manager.py +253 -0
- scrollkit/simulator/core/pixel_buffer.py +188 -0
- scrollkit/simulator/devices/__init__.py +7 -0
- scrollkit/simulator/devices/base_device.py +79 -0
- scrollkit/simulator/devices/matrixportal_s3.py +87 -0
- scrollkit/simulator/displayio/__init__.py +12 -0
- scrollkit/simulator/displayio/bitmap.py +158 -0
- scrollkit/simulator/displayio/display.py +195 -0
- scrollkit/simulator/displayio/fourwire.py +54 -0
- scrollkit/simulator/displayio/group.py +125 -0
- scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
- scrollkit/simulator/displayio/palette.py +115 -0
- scrollkit/simulator/displayio/tilegrid.py +155 -0
- scrollkit/simulator/fonts/3x5.bdf +2474 -0
- scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
- scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
- scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
- scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
- scrollkit/simulator/fonts/viii.bdf +2659 -0
- scrollkit/simulator/terminalio/__init__.py +20 -0
- scrollkit/utils/__init__.py +1 -0
- scrollkit/utils/color_utils.py +54 -0
- scrollkit/utils/diagnostics.py +227 -0
- scrollkit/utils/error_handler.py +347 -0
- scrollkit/utils/system_utils.py +245 -0
- scrollkit/utils/url_utils.py +46 -0
- scrollkit/web/__init__.py +6 -0
- scrollkit/web/settings_server.py +328 -0
- scrollkit/web/wifi_setup.py +331 -0
- scrollkit-0.8.3.dist-info/METADATA +248 -0
- scrollkit-0.8.3.dist-info/RECORD +111 -0
- scrollkit-0.8.3.dist-info/WHEEL +5 -0
- scrollkit-0.8.3.dist-info/licenses/LICENSE +31 -0
- scrollkit-0.8.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Machine-readable catalog of what a ScrollKit app can use — for AI authoring.
|
|
3
|
+
|
|
4
|
+
``capabilities()`` returns a JSON-able dict describing the building blocks an
|
|
5
|
+
agent should reach for when writing an app: the panel geometry, the content
|
|
6
|
+
types and their constructor parameters, priority levels, available effects,
|
|
7
|
+
named colors, and the display drawing API — plus a pointer at the headless
|
|
8
|
+
verification loop.
|
|
9
|
+
|
|
10
|
+
Everything is **introspected from live code** (the real ``Priority`` class, the
|
|
11
|
+
``effects`` package's ``__all__``, ``NAMED_COLORS``, the
|
|
12
|
+
``DisplayInterface`` methods, and the ``DisplayContent`` subclasses), so the
|
|
13
|
+
catalog can't silently drift out of sync with the library. Each lookup is
|
|
14
|
+
defensive: if a piece can't be imported, that section is simply omitted rather
|
|
15
|
+
than crashing the whole catalog.
|
|
16
|
+
|
|
17
|
+
Desktop-only (imported via ``scrollkit.dev``).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import inspect
|
|
21
|
+
|
|
22
|
+
# The Adafruit MatrixPortal S3's standard HUB75 panel — the canonical target the
|
|
23
|
+
# simulator emulates. (Color is 24-bit RGB; the panel itself is far coarser.)
|
|
24
|
+
PANEL_WIDTH = 64
|
|
25
|
+
PANEL_HEIGHT = 32
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _first_line(obj):
|
|
29
|
+
doc = inspect.getdoc(obj) or ""
|
|
30
|
+
return doc.strip().split("\n", 1)[0] if doc else ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _init_params(cls):
|
|
34
|
+
"""``[{name, default}]`` for a class __init__, skipping self/var-args."""
|
|
35
|
+
out = []
|
|
36
|
+
try:
|
|
37
|
+
sig = inspect.signature(cls.__init__)
|
|
38
|
+
except (ValueError, TypeError):
|
|
39
|
+
return out
|
|
40
|
+
for name, p in sig.parameters.items():
|
|
41
|
+
if name == "self" or p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
|
|
42
|
+
continue
|
|
43
|
+
default = None if p.default is inspect.Parameter.empty else p.default
|
|
44
|
+
out.append({"name": name, "default": default})
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _content_types():
|
|
49
|
+
from ..display import content as _content
|
|
50
|
+
base = _content.DisplayContent
|
|
51
|
+
types = []
|
|
52
|
+
for name in dir(_content):
|
|
53
|
+
obj = getattr(_content, name)
|
|
54
|
+
if (inspect.isclass(obj) and issubclass(obj, base)
|
|
55
|
+
and obj is not base
|
|
56
|
+
and obj.__module__ == _content.__name__):
|
|
57
|
+
types.append({"name": name, "doc": _first_line(obj),
|
|
58
|
+
"params": _init_params(obj),
|
|
59
|
+
"feasibility": getattr(obj, "FEASIBILITY", None)})
|
|
60
|
+
types.sort(key=lambda t: t["name"])
|
|
61
|
+
return types
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _text_fills():
|
|
65
|
+
"""Gradient text-fill capability for the Label-based content types.
|
|
66
|
+
|
|
67
|
+
The fill *params* (``palette``/``direction``/``palette_steps``) already surface
|
|
68
|
+
via ``_content_types`` signature introspection; this section adds the
|
|
69
|
+
machine-usable detail an author needs — the valid directions and step bounds —
|
|
70
|
+
read **live** from ``display.text_fill`` (never a hardcoded copy), so it can't
|
|
71
|
+
drift from the renderer.
|
|
72
|
+
"""
|
|
73
|
+
from ..display.text_fill import (DEFAULT_PALETTE_STEPS, MAX_PALETTE_STEPS,
|
|
74
|
+
gradient_directions)
|
|
75
|
+
return {
|
|
76
|
+
"gradient": {
|
|
77
|
+
"applies_to": ["StaticText", "ScrollingText"],
|
|
78
|
+
"params": ["palette", "direction", "palette_steps"],
|
|
79
|
+
"directions": list(gradient_directions()),
|
|
80
|
+
"default_direction": "vertical",
|
|
81
|
+
"default_palette_steps": DEFAULT_PALETTE_STEPS,
|
|
82
|
+
"max_palette_steps": MAX_PALETTE_STEPS,
|
|
83
|
+
"palette": ("a sequence of 2+ 0xRRGGBB stops (2 = simple gradient, 3+ "
|
|
84
|
+
"= multi-stop). When set, `color` is ignored. "
|
|
85
|
+
"depth_palette(color) derives a subtle close ramp from one "
|
|
86
|
+
"base colour. Static fill, zero per-frame cost — for "
|
|
87
|
+
"ANIMATED colour use BitmapText + a palette_effect."),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _color_utilities():
|
|
93
|
+
"""Continuous colour generators/transforms (NOT named palettes).
|
|
94
|
+
|
|
95
|
+
Surfaced as live signatures from ``display.colors`` so an author samples the
|
|
96
|
+
full 24-bit space rather than reaching for a fixed named subset.
|
|
97
|
+
"""
|
|
98
|
+
from ..display import colors as _c
|
|
99
|
+
out = []
|
|
100
|
+
for nm in ("gradient", "multi_gradient", "depth_palette", "spectrum",
|
|
101
|
+
"hsv", "scale", "lerp", "wheel"):
|
|
102
|
+
fn = getattr(_c, nm, None)
|
|
103
|
+
if fn is None:
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
sig = "%s%s" % (nm, inspect.signature(fn))
|
|
107
|
+
except (ValueError, TypeError):
|
|
108
|
+
sig = nm
|
|
109
|
+
out.append({"name": nm, "signature": sig, "doc": _first_line(fn)})
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _priorities():
|
|
114
|
+
from ..display.content import Priority
|
|
115
|
+
levels = {}
|
|
116
|
+
for name in dir(Priority):
|
|
117
|
+
if name.isupper() and isinstance(getattr(Priority, name), int):
|
|
118
|
+
levels[name] = getattr(Priority, name)
|
|
119
|
+
return dict(sorted(levels.items(), key=lambda kv: kv[1]))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _effects():
|
|
123
|
+
"""``[{name, doc}]`` for the standalone splash/particle effect classes.
|
|
124
|
+
|
|
125
|
+
Deep-imports each submodule directly (``effects/__init__.py`` is
|
|
126
|
+
deliberately import-free — see its docstring) so this catalog build never
|
|
127
|
+
loads a heavier module than the one it's cataloging.
|
|
128
|
+
"""
|
|
129
|
+
out = []
|
|
130
|
+
try:
|
|
131
|
+
from ..effects.particles import ParticleEngine, Sparkle, Snow
|
|
132
|
+
for name, obj in (("ParticleEngine", ParticleEngine),
|
|
133
|
+
("Sparkle", Sparkle), ("Snow", Snow)):
|
|
134
|
+
out.append({"name": name, "doc": _first_line(obj)})
|
|
135
|
+
except ImportError:
|
|
136
|
+
pass
|
|
137
|
+
try:
|
|
138
|
+
from ..effects.drip_splash import DripReveal
|
|
139
|
+
out.append({"name": "DripReveal", "doc": _first_line(DripReveal)})
|
|
140
|
+
except ImportError:
|
|
141
|
+
pass
|
|
142
|
+
try:
|
|
143
|
+
from ..effects.swarm_reveal import SwarmReveal
|
|
144
|
+
out.append({"name": "SwarmReveal", "doc": _first_line(SwarmReveal)})
|
|
145
|
+
except ImportError:
|
|
146
|
+
pass
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _transitions():
|
|
151
|
+
"""``[{name, doc, feasibility, pairs_with}]`` for the built-in content-swap transitions.
|
|
152
|
+
|
|
153
|
+
Enumerated via the explicit ``_TRANSITION_MAP`` (not ``Transition.__subclasses__``)
|
|
154
|
+
so the duck-typed ``DropFromSky`` is included and the user-facing names match the
|
|
155
|
+
settings UI. ``feasibility`` is each class's ``FEASIBILITY`` budget and
|
|
156
|
+
``pairs_with`` says which content style it suits (static / scrolling / fullscreen).
|
|
157
|
+
"""
|
|
158
|
+
from ..effects.transitions import _TRANSITION_MAP
|
|
159
|
+
out = []
|
|
160
|
+
for name, cls in _TRANSITION_MAP.items():
|
|
161
|
+
out.append({"name": name, "doc": _first_line(cls),
|
|
162
|
+
"feasibility": getattr(cls, "FEASIBILITY", None),
|
|
163
|
+
"pairs_with": list(getattr(cls, "PAIRS_WITH", ()))})
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _scrolling():
|
|
168
|
+
"""``[{name, doc, feasibility, pairs_with}]`` for the Class-1 scrolling effects.
|
|
169
|
+
|
|
170
|
+
These ARE content presentations (DisplayContent subclasses); ``pairs_with`` says
|
|
171
|
+
which content style each suits (see PAIRS_WITH in ``effects/scrolling.py``).
|
|
172
|
+
"""
|
|
173
|
+
out = []
|
|
174
|
+
try:
|
|
175
|
+
from ..effects import scrolling as _sc
|
|
176
|
+
except ImportError:
|
|
177
|
+
return out
|
|
178
|
+
for nm in ("KineticMarquee", "WaveRider", "SplitFlap"):
|
|
179
|
+
cls = getattr(_sc, nm, None)
|
|
180
|
+
if cls is not None:
|
|
181
|
+
out.append({"name": nm, "doc": _first_line(cls),
|
|
182
|
+
"feasibility": getattr(cls, "FEASIBILITY", None),
|
|
183
|
+
"pairs_with": list(getattr(cls, "PAIRS_WITH", ()))})
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _palette_effects():
|
|
188
|
+
"""``[{name, doc, applies_to, pairs_with}]`` for the BitmapText palette animations.
|
|
189
|
+
|
|
190
|
+
They animate the colour palette of bitmap text (no glyph rebuild); ``pairs_with``
|
|
191
|
+
says they read well on static or scrolling text.
|
|
192
|
+
"""
|
|
193
|
+
out = []
|
|
194
|
+
try:
|
|
195
|
+
from ..display import bitmap_text as _bt
|
|
196
|
+
except ImportError:
|
|
197
|
+
return out
|
|
198
|
+
for nm in ("RainbowChase", "NeonTubeCrawl", "ChromeSheen", "HazardStripes",
|
|
199
|
+
"MonoChase"):
|
|
200
|
+
cls = getattr(_bt, nm, None)
|
|
201
|
+
if cls is not None:
|
|
202
|
+
out.append({"name": nm, "doc": _first_line(cls),
|
|
203
|
+
"applies_to": "BitmapText",
|
|
204
|
+
"pairs_with": list(getattr(cls, "PAIRS_WITH", ()))})
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _named_colors():
|
|
209
|
+
"""name -> 0xRRGGBB int, from the tooling color table (deduped)."""
|
|
210
|
+
from ..utils.color_utils import NAMED_COLORS
|
|
211
|
+
colors = {}
|
|
212
|
+
for name, rgb in NAMED_COLORS.items():
|
|
213
|
+
try:
|
|
214
|
+
r, g, b = rgb
|
|
215
|
+
colors[name] = (int(r) << 16) | (int(g) << 8) | int(b)
|
|
216
|
+
except (TypeError, ValueError):
|
|
217
|
+
continue
|
|
218
|
+
return colors
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _display_api():
|
|
222
|
+
from ..display.interface import DisplayInterface
|
|
223
|
+
methods = []
|
|
224
|
+
for name in dir(DisplayInterface):
|
|
225
|
+
if name.startswith("_"):
|
|
226
|
+
continue
|
|
227
|
+
attr = inspect.getattr_static(DisplayInterface, name, None)
|
|
228
|
+
is_prop = isinstance(attr, property)
|
|
229
|
+
member = getattr(DisplayInterface, name, None)
|
|
230
|
+
if not (is_prop or inspect.isfunction(member) or inspect.iscoroutinefunction(member)):
|
|
231
|
+
continue
|
|
232
|
+
entry = {"name": name, "property": is_prop, "doc": _first_line(member)}
|
|
233
|
+
target = attr.fget if is_prop else member
|
|
234
|
+
try:
|
|
235
|
+
entry["signature"] = "%s%s" % (name, inspect.signature(target))
|
|
236
|
+
except (ValueError, TypeError):
|
|
237
|
+
entry["signature"] = name
|
|
238
|
+
methods.append(entry)
|
|
239
|
+
methods.sort(key=lambda m: m["name"])
|
|
240
|
+
return methods
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _hardware():
|
|
244
|
+
from ..simulator.core.hardware_profile import matrixportal_s3_profile
|
|
245
|
+
p = matrixportal_s3_profile()
|
|
246
|
+
ceiling = int(1_000_000 / p.full_refresh_us) if p.full_refresh_us else 0
|
|
247
|
+
return {
|
|
248
|
+
"target": p.name,
|
|
249
|
+
"usable_ram_bytes": p.usable_ram_bytes,
|
|
250
|
+
"confidence": p.confidence,
|
|
251
|
+
"calibrated": p.is_calibrated,
|
|
252
|
+
"refresh_fps_ceiling": ceiling,
|
|
253
|
+
"guidance": ("Every frame pays one display.refresh() (~%d us, a hard "
|
|
254
|
+
"ceiling near %d FPS) plus a glyph-bitmap rebuild for any "
|
|
255
|
+
"text that changed. Redrawing many text fields every frame "
|
|
256
|
+
"is the main way to fall to single-digit FPS — cache Labels "
|
|
257
|
+
"and only change .text when the value changes. RAM budget is "
|
|
258
|
+
"~%d KB." % (round(p.full_refresh_us), ceiling,
|
|
259
|
+
p.usable_ram_bytes // 1024)),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _performance():
|
|
264
|
+
from .performance import performance_guide
|
|
265
|
+
return performance_guide()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def capabilities():
|
|
269
|
+
"""Return the JSON-able capability catalog (see module docstring).
|
|
270
|
+
|
|
271
|
+
Sections are best-effort: any that can't be introspected is omitted, so the
|
|
272
|
+
call never fails just because one optional submodule is missing.
|
|
273
|
+
"""
|
|
274
|
+
cat = {
|
|
275
|
+
"panel": {
|
|
276
|
+
"width": PANEL_WIDTH,
|
|
277
|
+
"height": PANEL_HEIGHT,
|
|
278
|
+
"color": "24-bit RGB int 0xRRGGBB, or an (r, g, b) tuple (0-255 each)",
|
|
279
|
+
"note": "Adafruit MatrixPortal S3 standard 64x32 panel",
|
|
280
|
+
"coordinates": {
|
|
281
|
+
"origin": "top-left",
|
|
282
|
+
"x": "rightward, 0..%d" % (PANEL_WIDTH - 1),
|
|
283
|
+
"y": "downward, 0..%d" % (PANEL_HEIGHT - 1),
|
|
284
|
+
"y_anchor": "baseline",
|
|
285
|
+
"note": ("(0, 0) is the top-left corner; x grows right, y grows "
|
|
286
|
+
"down (standard displayio). y sets the text baseline, "
|
|
287
|
+
"not the glyph top, so y=0 clips a line off the top. "
|
|
288
|
+
"For the 8px font, y=12 vertically centers one line."),
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
"verification": (
|
|
292
|
+
"Build a scrollkit.app.base.ScrollKitApp subclass, then run it "
|
|
293
|
+
"headless with scrollkit.dev.run_headless(app, frames=N, "
|
|
294
|
+
"screenshot=path) to get rendered-pixel metrics and an estimated "
|
|
295
|
+
"hardware feasibility report. Use scrollkit.dev.validate(app) for "
|
|
296
|
+
"structured pre-flight checks."
|
|
297
|
+
),
|
|
298
|
+
}
|
|
299
|
+
for key, fn in (("content_types", _content_types), ("priorities", _priorities),
|
|
300
|
+
("effects", _effects), ("transitions", _transitions),
|
|
301
|
+
("scrolling", _scrolling), ("palette_effects", _palette_effects),
|
|
302
|
+
("text_fills", _text_fills), ("color_utilities", _color_utilities),
|
|
303
|
+
("named_colors", _named_colors),
|
|
304
|
+
("display_api", _display_api), ("hardware", _hardware),
|
|
305
|
+
("performance", _performance)):
|
|
306
|
+
try:
|
|
307
|
+
cat[key] = fn()
|
|
308
|
+
except Exception as e: # one bad section shouldn't sink the catalog
|
|
309
|
+
cat[key] = {"error": "introspection failed: %r" % (e,)}
|
|
310
|
+
return cat
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def as_text(cat=None):
|
|
314
|
+
"""Render the catalog as a compact human/AI-readable summary."""
|
|
315
|
+
cat = cat or capabilities()
|
|
316
|
+
lines = ["=== ScrollKit capabilities ==="]
|
|
317
|
+
p = cat.get("panel", {})
|
|
318
|
+
lines.append("Panel: %sx%s, color=%s" % (p.get("width"), p.get("height"),
|
|
319
|
+
p.get("color")))
|
|
320
|
+
coords = p.get("coordinates")
|
|
321
|
+
if isinstance(coords, dict) and coords.get("note"):
|
|
322
|
+
lines.append("Coordinates: %s" % coords["note"])
|
|
323
|
+
ct = cat.get("content_types")
|
|
324
|
+
if isinstance(ct, list):
|
|
325
|
+
lines.append("Content types:")
|
|
326
|
+
for t in ct:
|
|
327
|
+
params = ", ".join(
|
|
328
|
+
"%s=%r" % (q["name"], q["default"]) if q["default"] is not None
|
|
329
|
+
else q["name"] for q in t["params"])
|
|
330
|
+
lines.append(" - %s(%s) — %s" % (t["name"], params, t["doc"]))
|
|
331
|
+
pr = cat.get("priorities")
|
|
332
|
+
if isinstance(pr, dict):
|
|
333
|
+
lines.append("Priorities: " + ", ".join("%s=%d" % (k, v)
|
|
334
|
+
for k, v in pr.items()))
|
|
335
|
+
fx = cat.get("effects")
|
|
336
|
+
if isinstance(fx, list) and fx:
|
|
337
|
+
lines.append("Effects: " + ", ".join(e["name"] for e in fx))
|
|
338
|
+
def _pairs(e):
|
|
339
|
+
pw = e.get("pairs_with")
|
|
340
|
+
return " [best on: %s]" % ", ".join(pw) if pw else ""
|
|
341
|
+
tr = cat.get("transitions")
|
|
342
|
+
if isinstance(tr, list) and tr:
|
|
343
|
+
lines.append("Transitions (transition_style setting):")
|
|
344
|
+
for t in tr:
|
|
345
|
+
feas = t.get("feasibility") or {}
|
|
346
|
+
ms = feas.get("modeled_frame_ms")
|
|
347
|
+
budget = " (~%sms/frame)" % ms if ms is not None else ""
|
|
348
|
+
lines.append(" - %s%s%s — %s" % (t["name"], budget, _pairs(t), t["doc"]))
|
|
349
|
+
sc = cat.get("scrolling")
|
|
350
|
+
if isinstance(sc, list) and sc:
|
|
351
|
+
lines.append("Scrolling effects:")
|
|
352
|
+
for e in sc:
|
|
353
|
+
lines.append(" - %s%s — %s" % (e["name"], _pairs(e), e["doc"]))
|
|
354
|
+
pe = cat.get("palette_effects")
|
|
355
|
+
if isinstance(pe, list) and pe:
|
|
356
|
+
lines.append("Palette effects (on BitmapText):")
|
|
357
|
+
for e in pe:
|
|
358
|
+
lines.append(" - %s%s — %s" % (e["name"], _pairs(e), e["doc"]))
|
|
359
|
+
tf = cat.get("text_fills")
|
|
360
|
+
if isinstance(tf, dict) and isinstance(tf.get("gradient"), dict):
|
|
361
|
+
g = tf["gradient"]
|
|
362
|
+
lines.append("Text fills: gradient palette=(c1,c2[,...]) direction=%s on %s"
|
|
363
|
+
% ("|".join(g.get("directions", [])),
|
|
364
|
+
"/".join(g.get("applies_to", []))))
|
|
365
|
+
cu = cat.get("color_utilities")
|
|
366
|
+
if isinstance(cu, list) and cu:
|
|
367
|
+
lines.append("Color utilities: " + ", ".join(u["name"] for u in cu))
|
|
368
|
+
nc = cat.get("named_colors")
|
|
369
|
+
if isinstance(nc, dict) and nc:
|
|
370
|
+
lines.append("Named colors: " + ", ".join(sorted(nc)))
|
|
371
|
+
hw = cat.get("hardware")
|
|
372
|
+
if isinstance(hw, dict) and "guidance" in hw:
|
|
373
|
+
lines.append("Hardware: %s" % hw["guidance"])
|
|
374
|
+
return "\n".join(lines)
|