mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
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>
89 lines
3.0 KiB
Python
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()
|