mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
Fix inconsistent Vegas scroll transition gaps caused by plugin-baked padding (#384)
* Strip plugin-baked scroll padding when capturing content for Vegas mode Plugins that build their own ticker image via ScrollHelper.create_scrolling_image() (or that manually pad both ends for a clean standalone loop) carry a solid-black margin up to display_width wide on one or both edges. Vegas mode already adds its own configurable gap around every item, so leaving that margin in place stacked an extra, uncontrolled blank stretch on top of separator_width for whichever plugin took the ScrollHelper-capture path — producing inconsistent transition gaps between modules compared to plugins that provide content natively via get_vegas_content(). _get_scroll_helper_content() now detects and crops any such margin before handing the image to the Vegas render pipeline, so every plugin's gap is governed solely by vegas_scroll.separator_width regardless of which capture path produced its content. * Address CodeRabbit nitpick: warn on double-edge padding crop, add unit tests Logging a double-edge match at warning level (vs. info for a single edge) makes it easy to spot an unexpected crop in the field, since two edges matching at once is a much stronger signal of genuine baked-in padding than one edge coinciding with real all-black content. Also adds test/test_vegas_plugin_adapter.py covering _strip_scroll_padding's branch logic: leading-only, trailing-only, both-edges, no-match, degenerate all-black, missing/undersized display_width, and the info-vs-warning log level. * Add type hints and docstring to test _solid helper (CodeRabbit nitpick) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Tests for src/vegas_mode/plugin_adapter.py
|
||||
|
||||
Covers PluginAdapter._strip_scroll_padding(): the heuristic that crops a
|
||||
plugin's own baked-in leading/trailing blank margins before Vegas mode
|
||||
composites the content, so vegas_scroll.separator_width is the only gap
|
||||
applied between items.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from src.common.scroll_helper import ScrollHelper
|
||||
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||
|
||||
|
||||
class FakeDisplayManager:
|
||||
width = 64
|
||||
height = 32
|
||||
|
||||
|
||||
class FakePlugin:
|
||||
def __init__(self, scroll_helper):
|
||||
self.scroll_helper = scroll_helper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter():
|
||||
return PluginAdapter(FakeDisplayManager())
|
||||
|
||||
|
||||
def _solid(width: int, height: int, color: tuple) -> Image.Image:
|
||||
"""Create a solid-color RGB image of the given dimensions."""
|
||||
return Image.new('RGB', (width, height), color)
|
||||
|
||||
|
||||
class TestStripScrollPadding:
|
||||
def test_leading_pad_from_create_scrolling_image_is_stripped(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
item = _solid(40, 32, (200, 50, 50))
|
||||
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
assert images[0].width == 40
|
||||
assert images[0].getpixel((0, 0)) == (200, 50, 50)
|
||||
|
||||
def test_leading_and_trailing_pad_both_stripped(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
content_w = 80
|
||||
full = _solid(64 + content_w + 64, 32, (0, 0, 0))
|
||||
full.paste(_solid(content_w, 32, (10, 220, 30)), (64, 0))
|
||||
sh.set_scrolling_image(full)
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
assert images[0].width == content_w
|
||||
assert images[0].getpixel((0, 0)) == (10, 220, 30)
|
||||
assert images[0].getpixel((content_w - 1, 0)) == (10, 220, 30)
|
||||
|
||||
def test_leading_only_pad_stripped_trailing_content_kept(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
content_w = 80
|
||||
full = _solid(64 + content_w, 32, (0, 0, 0))
|
||||
full.paste(_solid(content_w, 32, (5, 5, 250)), (64, 0))
|
||||
sh.set_scrolling_image(full)
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
assert images[0].width == content_w
|
||||
assert images[0].getpixel((0, 0)) == (5, 5, 250)
|
||||
|
||||
def test_trailing_only_pad_stripped_leading_content_kept(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
content_w = 80
|
||||
full = _solid(content_w + 64, 32, (0, 0, 0))
|
||||
full.paste(_solid(content_w, 32, (5, 5, 250)), (0, 0))
|
||||
sh.set_scrolling_image(full)
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
assert images[0].width == content_w
|
||||
assert images[0].getpixel((0, 0)) == (5, 5, 250)
|
||||
|
||||
def test_no_margin_image_left_untouched(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
raw = _solid(150, 32, (5, 5, 5))
|
||||
raw.paste(_solid(50, 32, (123, 45, 67)), (0, 0))
|
||||
sh.set_scrolling_image(raw)
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "no_margin")
|
||||
assert images[0].width == 150
|
||||
|
||||
def test_degenerate_all_black_image_left_untouched(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
sh.set_scrolling_image(_solid(50, 32, (0, 0, 0)))
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "all_black")
|
||||
assert images[0].width == 50
|
||||
|
||||
def test_missing_display_width_attribute_left_untouched(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
item = _solid(40, 32, (200, 50, 50))
|
||||
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||
original_width = sh.cached_image.width
|
||||
del sh.display_width
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
assert images[0].width == original_width
|
||||
|
||||
def test_pad_width_not_smaller_than_image_left_untouched(self, adapter):
|
||||
sh = ScrollHelper(64, 32)
|
||||
sh.set_scrolling_image(_solid(64, 32, (0, 0, 0)))
|
||||
|
||||
images = adapter._get_scroll_helper_content(FakePlugin(sh), "narrow")
|
||||
assert images[0].width == 64
|
||||
|
||||
def test_both_edges_matching_logs_warning(self, adapter, caplog):
|
||||
sh = ScrollHelper(64, 32)
|
||||
content_w = 80
|
||||
full = _solid(64 + content_w + 64, 32, (0, 0, 0))
|
||||
full.paste(_solid(content_w, 32, (10, 220, 30)), (64, 0))
|
||||
sh.set_scrolling_image(full)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="src.vegas_mode.plugin_adapter"):
|
||||
adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
|
||||
assert any("Stripping scroll_helper padding" in r.message for r in caplog.records)
|
||||
|
||||
def test_single_edge_match_logs_info_not_warning(self, adapter, caplog):
|
||||
sh = ScrollHelper(64, 32)
|
||||
item = _solid(40, 32, (200, 50, 50))
|
||||
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||
|
||||
with caplog.at_level(logging.INFO, logger="src.vegas_mode.plugin_adapter"):
|
||||
adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||
|
||||
strip_records = [r for r in caplog.records if "Stripping scroll_helper padding" in r.message]
|
||||
assert len(strip_records) == 1
|
||||
assert strip_records[0].levelno == logging.INFO
|
||||
Reference in New Issue
Block a user