mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user