Applies gradients derived from 103 bundled Philips Hue scenes to OpenRGB-controlled LEDs, with six mapping modes (sequence, per-device, per-zone, matrix, mirror, shuffle) and five animation modes (static, scroll, pingpong, pulse, wave). Typer/Rich CLI with dry-run preview, device/zone targeting, and a scenes-update command to refresh the bundled dataset from its source gist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
import pytest
|
|
|
|
from openrgb_hue.color import Color
|
|
from openrgb_hue.gradient import Gradient
|
|
from openrgb_hue.mapping import (
|
|
FrameParams,
|
|
MatrixMapping,
|
|
MirrorMapping,
|
|
PerDeviceMapping,
|
|
PerZoneMapping,
|
|
SequenceMapping,
|
|
ShuffleMapping,
|
|
UnknownMappingModeError,
|
|
base_positions,
|
|
build_mapping,
|
|
render_frame,
|
|
)
|
|
from openrgb_hue.targets import synthetic_targets
|
|
|
|
|
|
def test_sequence_mapping_linear_spacing(synthetic_leds):
|
|
positions = SequenceMapping().positions(synthetic_leds)
|
|
ordered = [positions[led] for led in synthetic_leds]
|
|
assert ordered[0] == 0.0
|
|
assert ordered[-1] == 1.0
|
|
assert ordered == sorted(ordered)
|
|
|
|
|
|
def test_sequence_mapping_single_target_is_zero():
|
|
leds = synthetic_targets(1)
|
|
positions = SequenceMapping().positions(leds)
|
|
assert positions[leds[0]] == 0.0
|
|
|
|
|
|
def test_per_device_mapping_each_device_spans_full_range():
|
|
leds = synthetic_targets() # two linear devices + one matrix device
|
|
positions = PerDeviceMapping().positions(leds)
|
|
for device_index in {led.device_index for led in leds}:
|
|
device_leds = [led for led in leds if led.device_index == device_index]
|
|
values = [positions[led] for led in device_leds]
|
|
assert min(values) == 0.0
|
|
assert max(values) == 1.0
|
|
|
|
|
|
def test_per_zone_mapping_groups_by_zone():
|
|
leds = synthetic_targets()
|
|
positions = PerZoneMapping().positions(leds)
|
|
for key in {(led.device_index, led.zone_index) for led in leds}:
|
|
zone_leds = [led for led in leds if (led.device_index, led.zone_index) == key]
|
|
values = [positions[led] for led in zone_leds]
|
|
assert min(values) == 0.0
|
|
assert max(values) == 1.0
|
|
|
|
|
|
@pytest.fixture
|
|
def matrix_leds():
|
|
return [led for led in synthetic_targets() if led.matrix_row is not None]
|
|
|
|
|
|
def test_matrix_mapping_left_right(matrix_leds):
|
|
positions = MatrixMapping(direction="left-right").positions(matrix_leds)
|
|
top_left = next(led for led in matrix_leds if led.matrix_row == 0 and led.matrix_col == 0)
|
|
top_right = next(led for led in matrix_leds if led.matrix_row == 0 and led.matrix_col == 3)
|
|
assert positions[top_left] == 0.0
|
|
assert positions[top_right] == 1.0
|
|
|
|
|
|
def test_matrix_mapping_top_bottom(matrix_leds):
|
|
positions = MatrixMapping(direction="top-bottom").positions(matrix_leds)
|
|
top = next(led for led in matrix_leds if led.matrix_row == 0 and led.matrix_col == 0)
|
|
bottom = next(led for led in matrix_leds if led.matrix_row == 3 and led.matrix_col == 0)
|
|
assert positions[top] == 0.0
|
|
assert positions[bottom] == 1.0
|
|
|
|
|
|
def test_matrix_mapping_diagonal(matrix_leds):
|
|
positions = MatrixMapping(direction="diagonal").positions(matrix_leds)
|
|
corner_near = next(led for led in matrix_leds if led.matrix_row == 0 and led.matrix_col == 0)
|
|
corner_far = next(led for led in matrix_leds if led.matrix_row == 3 and led.matrix_col == 3)
|
|
assert positions[corner_near] == 0.0
|
|
assert positions[corner_far] == 1.0
|
|
|
|
|
|
def test_matrix_mapping_radial_center_is_minimum(matrix_leds):
|
|
positions = MatrixMapping(direction="radial").positions(matrix_leds)
|
|
# On a 4x4 grid the center falls between cells; corners should be
|
|
# further from center than the near-center cells.
|
|
corner = next(led for led in matrix_leds if led.matrix_row == 0 and led.matrix_col == 0)
|
|
near_center = next(led for led in matrix_leds if led.matrix_row == 1 and led.matrix_col == 1)
|
|
assert positions[near_center] < positions[corner]
|
|
|
|
|
|
def test_matrix_mapping_invalid_direction_raises():
|
|
with pytest.raises(ValueError):
|
|
MatrixMapping(direction="sideways")
|
|
|
|
|
|
def test_matrix_mapping_warns_on_non_matrix_leds():
|
|
leds = synthetic_targets(4) # flat LEDs, no matrix coordinates
|
|
warnings = []
|
|
MatrixMapping(warn=warnings.append).positions(leds)
|
|
assert len(warnings) == 1
|
|
assert all(led.matrix_row is None for led in leds)
|
|
|
|
|
|
def test_mirror_mapping_is_symmetric(synthetic_leds):
|
|
positions = MirrorMapping().positions(synthetic_leds)
|
|
values = [positions[led] for led in synthetic_leds]
|
|
n = len(values)
|
|
for i in range(n):
|
|
assert values[i] == pytest.approx(values[n - 1 - i])
|
|
|
|
|
|
def test_shuffle_mapping_deterministic_and_seed_sensitive(synthetic_leds):
|
|
a = ShuffleMapping(seed=1).positions(synthetic_leds)
|
|
b = ShuffleMapping(seed=1).positions(synthetic_leds)
|
|
c = ShuffleMapping(seed=2).positions(synthetic_leds)
|
|
assert a == b
|
|
assert a != c
|
|
|
|
|
|
def test_build_mapping_unknown_name_raises():
|
|
with pytest.raises(UnknownMappingModeError):
|
|
build_mapping("not-a-real-mode")
|
|
|
|
|
|
def test_render_frame_uses_gradient_and_offset(synthetic_leds):
|
|
gradient = Gradient.from_colors([Color(255, 0, 0), Color(0, 0, 255)], interpolation="rgb")
|
|
base = base_positions(SequenceMapping(), synthetic_leds)
|
|
colors = render_frame(base, gradient, FrameParams(t_offset=0.0, brightness=1.0))
|
|
assert colors[synthetic_leds[0]] == gradient.sample(0.0)
|
|
|
|
|
|
def test_render_frame_brightness_fn_overrides_scalar_brightness(synthetic_leds):
|
|
gradient = Gradient.from_colors([Color(255, 255, 255)], interpolation="rgb")
|
|
base = base_positions(SequenceMapping(), synthetic_leds)
|
|
params = FrameParams(brightness=1.0, brightness_fn=lambda t: 0.0)
|
|
colors = render_frame(base, gradient, params)
|
|
assert all(c == Color(0, 0, 0) for c in colors.values())
|