Ship the portal FastAPI controller with presets, sequences, and live audio.
Move the app under src/, add ring-spanning patterns with shared fonts and beat-driven motion, and wire global audio settings into the web UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
"""Pattern plugin smoke tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(__file__).resolve().parent.parent / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from leds.panel_udp import HeadlessMatrix
|
||||
from leds.pattern import get_registry, merge_params
|
||||
|
||||
|
||||
NEW_PATTERN_IDS = (
|
||||
"aurora",
|
||||
"meteor",
|
||||
"embers",
|
||||
"tide",
|
||||
"vortex",
|
||||
"serpent",
|
||||
"lattice",
|
||||
"eclipse",
|
||||
)
|
||||
|
||||
|
||||
def test_registry_loads_bundled_patterns() -> None:
|
||||
ids = get_registry().ids()
|
||||
assert "rainbow" in ids
|
||||
assert "scanner" in ids
|
||||
for pattern_id in NEW_PATTERN_IDS:
|
||||
assert pattern_id in ids, pattern_id
|
||||
assert len(ids) >= 18
|
||||
|
||||
|
||||
def test_scanner_draws_pixels() -> None:
|
||||
cls = get_registry()["scanner"]
|
||||
inst = cls()
|
||||
params = merge_params(cls, {"colors": {"beam": "#00ff00"}})
|
||||
matrix = HeadlessMatrix(20, 9, brightness=1.0)
|
||||
inst.draw(matrix, 0.5, params)
|
||||
assert any(matrix.rgb_bytes())
|
||||
|
||||
|
||||
def test_ancient_text_patterns_draw() -> None:
|
||||
registry = get_registry()
|
||||
for pattern_id in (
|
||||
"inscription",
|
||||
"runes",
|
||||
"glyphs",
|
||||
"oracle",
|
||||
"procession",
|
||||
"rite",
|
||||
"braziers",
|
||||
"constellation",
|
||||
"veil",
|
||||
*NEW_PATTERN_IDS,
|
||||
):
|
||||
assert pattern_id in registry
|
||||
cls = registry[pattern_id]
|
||||
matrix = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
cls().draw(matrix, 1.25, merge_params(cls))
|
||||
assert max(matrix.rgb_bytes()) > 40, pattern_id
|
||||
|
||||
|
||||
def test_portal_ring_spans_panels() -> None:
|
||||
from leds.array_config import PANEL_COUNT, panel_layout
|
||||
from leds.portal_span import inject_portal, portal_ring_width
|
||||
|
||||
cls = get_registry()["procession"]
|
||||
inst = cls()
|
||||
params = merge_params(cls)
|
||||
lit_global = set()
|
||||
for index in range(PANEL_COUNT):
|
||||
width, height = panel_layout(index)
|
||||
matrix = HeadlessMatrix(width, height, brightness=1.0)
|
||||
inject_portal(params, index)
|
||||
inst.draw(matrix, 2.5, params)
|
||||
rgb = matrix.rgb_bytes()
|
||||
offset = params["portal"]["x_offset"]
|
||||
for x in range(width):
|
||||
# Middle row brightness sample.
|
||||
i = ((height // 2) * width + x) * 3
|
||||
if rgb[i] + rgb[i + 1] + rgb[i + 2] > 30:
|
||||
lit_global.add(offset + x)
|
||||
# Spanning pattern should light pixels across more than one panel's width.
|
||||
assert len(lit_global) > 50
|
||||
assert params["portal"]["ring_width"] == portal_ring_width()
|
||||
|
||||
|
||||
def test_ancient_patterns_have_no_preset_bpm() -> None:
|
||||
for pattern_id in (
|
||||
"inscription",
|
||||
"runes",
|
||||
"glyphs",
|
||||
"oracle",
|
||||
"rite",
|
||||
"veil",
|
||||
"procession",
|
||||
"braziers",
|
||||
"constellation",
|
||||
"aurora",
|
||||
"vortex",
|
||||
"serpent",
|
||||
"lattice",
|
||||
"eclipse",
|
||||
"tide",
|
||||
"embers",
|
||||
):
|
||||
names = {p["name"] for p in get_registry()[pattern_id].schema()["params"]}
|
||||
assert "bpm" not in names, pattern_id
|
||||
|
||||
|
||||
def test_resolve_bpm_uses_settings_fallback() -> None:
|
||||
from leds.bpm_limits import resolve_bpm
|
||||
|
||||
params = merge_params(get_registry()["rite"])
|
||||
params["beat"] = {"active": False, "fallback_bpm": 140}
|
||||
assert resolve_bpm(params, fallback=120) == 140
|
||||
params["extras"]["beat_detect"] = True
|
||||
params["beat"] = {"active": True, "bpm": 128, "fallback_bpm": 140}
|
||||
assert resolve_bpm(params, fallback=120) == 128
|
||||
params["extras"]["beat_detect"] = False
|
||||
assert resolve_bpm(params, fallback=120) == 140
|
||||
|
||||
|
||||
def test_no_synthetic_pulse_when_beat_detect_off() -> None:
|
||||
from leds.bpm_limits import resolve_beat_wave, resolve_pulse
|
||||
|
||||
params = merge_params(get_registry()["rite"])
|
||||
assert params["extras"]["beat_detect"] is False
|
||||
params["beat"] = {"active": True, "bpm": 128, "pulse": 0.9, "fallback_bpm": 120}
|
||||
assert resolve_pulse(params, 0.25, fallback_bpm=120) == 0.0
|
||||
assert resolve_beat_wave(params, 0.25, fallback_bpm=120) == 0.5
|
||||
|
||||
|
||||
def test_patterns_prefer_live_beat_pulse() -> None:
|
||||
from leds.bpm_limits import resolve_pulse
|
||||
|
||||
cls = get_registry()["rite"]
|
||||
params = merge_params(cls)
|
||||
params["extras"]["beat_detect"] = True
|
||||
params["beat"] = {
|
||||
"active": True,
|
||||
"bpm": 128,
|
||||
"pulse": 0.9,
|
||||
"beat_seq": 12,
|
||||
"kick": 0.8,
|
||||
"bass": 0.6,
|
||||
"energy": 0.7,
|
||||
"flux": 0.2,
|
||||
"hat": 0.1,
|
||||
"snare": 0.3,
|
||||
"level": 0.4,
|
||||
}
|
||||
assert resolve_pulse(params, 0.0, fallback_bpm=120) == 0.9
|
||||
matrix = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
cls().draw(matrix, 1.0, params)
|
||||
assert max(matrix.rgb_bytes()) > 40
|
||||
|
||||
|
||||
|
||||
def test_inscription_accepts_custom_text() -> None:
|
||||
cls = get_registry()["inscription"]
|
||||
schema = cls.schema()
|
||||
text_param = next(p for p in schema["params"] if p["name"] == "text")
|
||||
assert text_param["type"] == "text"
|
||||
params = merge_params(cls, {"extras": {"text": "ave"}})
|
||||
assert params["extras"]["text"] == "ave"
|
||||
matrix = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
cls().draw(matrix, 0.0, params)
|
||||
assert max(matrix.rgb_bytes()) > 40
|
||||
|
||||
|
||||
def test_oracle_splits_custom_words() -> None:
|
||||
from patterns.oracle import _words
|
||||
|
||||
assert _words("lux / nox") == ("LUX", "NOX")
|
||||
cls = get_registry()["oracle"]
|
||||
params = merge_params(cls, {"extras": {"text": "LUX", "beats_per_word": 3.0}})
|
||||
assert params["extras"]["text"] == "LUX"
|
||||
assert params["extras"]["beats_per_word"] == 3.0
|
||||
matrix = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
cls().draw(matrix, 0.5, params)
|
||||
assert max(matrix.rgb_bytes()) > 40
|
||||
|
||||
|
||||
def test_preset_normalize_keeps_text() -> None:
|
||||
from leds.presets import _normalize
|
||||
|
||||
preset = _normalize(
|
||||
{
|
||||
"name": "Hello",
|
||||
"pattern": "inscription",
|
||||
"brightness": 0.4,
|
||||
"speed": 1.0,
|
||||
"colors": {"ink": "#e0b44a", "stone": "#2a1c10"},
|
||||
"extras": {"text": "HELLO"},
|
||||
}
|
||||
)
|
||||
assert preset["extras"]["text"] == "HELLO"
|
||||
|
||||
|
||||
def test_new_pattern_presets_normalize() -> None:
|
||||
from leds.presets import _normalize
|
||||
|
||||
preset = _normalize(
|
||||
{
|
||||
"name": "Vortex",
|
||||
"pattern": "vortex",
|
||||
"brightness": 0.42,
|
||||
"speed": 1.0,
|
||||
"colors": {"core": "#6a20ff", "rim": "#f0c060", "void": "#05030a"},
|
||||
"extras": {"twist": 0.75, "bg_brightness": 0.4},
|
||||
}
|
||||
)
|
||||
assert preset["pattern"] == "vortex"
|
||||
assert preset["extras"]["twist"] == 0.75
|
||||
|
||||
|
||||
def test_text_and_background_brightness() -> None:
|
||||
cls = get_registry()["inscription"]
|
||||
names = {p["name"] for p in cls.schema()["params"]}
|
||||
assert "text_brightness" in names
|
||||
assert "bg_brightness" in names
|
||||
bright = merge_params(cls, {"extras": {"text": "AVE", "text_brightness": 1.0, "bg_brightness": 0.0}})
|
||||
dimmed = merge_params(cls, {"extras": {"text": "AVE", "text_brightness": 0.0, "bg_brightness": 0.0}})
|
||||
a = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
b = HeadlessMatrix(45, 9, brightness=1.0)
|
||||
cls().draw(a, 0.0, bright)
|
||||
cls().draw(b, 0.0, dimmed)
|
||||
assert sum(a.rgb_bytes()) > sum(b.rgb_bytes())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_registry_loads_bundled_patterns()
|
||||
test_scanner_draws_pixels()
|
||||
test_ancient_text_patterns_draw()
|
||||
test_portal_ring_spans_panels()
|
||||
test_ancient_patterns_have_no_preset_bpm()
|
||||
test_resolve_bpm_uses_settings_fallback()
|
||||
test_no_synthetic_pulse_when_beat_detect_off()
|
||||
test_patterns_prefer_live_beat_pulse()
|
||||
test_inscription_accepts_custom_text()
|
||||
test_oracle_splits_custom_words()
|
||||
test_preset_normalize_keeps_text()
|
||||
test_text_and_background_brightness()
|
||||
test_new_pattern_presets_normalize()
|
||||
print("ok")
|
||||
Reference in New Issue
Block a user