Files
LEDMatrix/test/test_scroll_helper_patch.py
T
ChuckandClaude Opus 5.5 876130e93f feat(vegas): live elements update in place while they scroll
One background worker (src/vegas_mode/live_worker.py) redraws a plugin's
live elements when its data epoch moves on (update listener) or on their
refresh_hz, nearest the screen first, and hands changed pixels lock-free to
the render thread, which copies them into the strip between frames
(RenderPipeline.apply_live_patches, ScrollHelper.patch_columns): at most
four patches or two screens of bytes a frame, no drawing or locks there.
The worker takes over group prefetch once a live element is placed, runs
inside the render gate, and is supervised. Update tick 1s while live
elements exist. Web UI switch for live_refresh. OFFSCREEN_RENDERING.md
describes what was built and why SegmentStrip was not needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:36:39 -04:00

89 lines
3.0 KiB
Python

"""ScrollHelper.patch_columns: changing a strip's pixels in place, between frames.
The primitive a live Vegas element update is built on. It writes exactly the
columns it is given and nothing else, never moves the scroll or resizes the
strip, and refuses an array it may not write (the multi-display follower's).
"""
import sys
from pathlib import Path
import numpy as np
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.common.scroll_helper import ScrollHelper # noqa: E402
W, H = 64, 16
def _helper(width=300):
rng = np.random.default_rng(1)
pixels = rng.integers(0, 255, (H, width, 3), dtype=np.uint8)
helper = ScrollHelper(W, H)
helper.set_scrolling_image(Image.frombytes("RGB", (width, H), pixels.tobytes()))
helper.scroll_position = 42.0
return helper
def _patch(width, value=7):
return np.full((H, width, 3), value, dtype=np.uint8)
def test_writes_exactly_its_columns():
helper = _helper()
before = helper.cached_array.copy()
assert helper.patch_columns(100, _patch(30)) == H * 30 * 3
after = helper.cached_array
assert (after[:, 100:130] == 7).all()
assert np.array_equal(after[:, :100], before[:, :100])
assert np.array_equal(after[:, 130:], before[:, 130:])
def test_moves_nothing_else():
helper = _helper()
width, position, distance = (helper.total_scroll_width, helper.scroll_position,
helper.total_distance_scrolled)
helper.patch_columns(10, _patch(20))
assert (helper.total_scroll_width, helper.scroll_position,
helper.total_distance_scrolled) == (width, position, distance)
def test_clips_at_both_ends():
helper = _helper(width=300)
assert helper.patch_columns(-10, _patch(30, 1)) == H * 20 * 3
assert (helper.cached_array[:, :20] == 1).all()
assert helper.patch_columns(290, _patch(30, 2)) == H * 10 * 3
assert (helper.cached_array[:, 290:] == 2).all()
assert helper.patch_columns(400, _patch(10)) == 0
assert helper.patch_columns(-50, _patch(10)) == 0
def test_the_patch_lands_in_the_next_frame():
helper = _helper()
helper.scroll_position = 100.0
helper.patch_columns(110, _patch(10, 200))
frame = np.asarray(helper.get_visible_portion())
assert (frame[:, 10:20] == 200).all()
def test_refuses_a_read_only_strip():
# The follower adopts np.asarray(image), which is read-only.
helper = _helper()
helper.cached_array = np.asarray(helper.cached_image)
assert not helper.cached_array.flags.writeable
assert helper.patch_columns(10, _patch(10)) == 0
def test_refuses_a_patch_of_the_wrong_height_and_no_strip():
helper = _helper()
assert helper.patch_columns(10, np.zeros((H + 1, 5, 3), dtype=np.uint8)) == 0
assert ScrollHelper(W, H).patch_columns(0, _patch(5)) == 0
def test_a_deferred_image_is_built_from_the_patched_strip():
helper = _helper()
helper.append_content([Image.new("RGB", (40, H))], item_gap=0)
helper.patch_columns(5, _patch(5, 99))
assert (np.asarray(helper.cached_image)[:, 5:10] == 99).all()