mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
chore(scroll): drop the dead sub-pixel path, and two dev-tooling papercuts (#570)
Three independent changes, none of which alter runtime behaviour. 1. Remove ScrollHelper._get_visible_portion_subpixel and _interpolate_subpixel (162 lines). get_visible_portion dispatches only to _blend_visible_portion, so _get_visible_portion_subpixel had no caller, and _interpolate_subpixel was reachable only from inside it -- a closed island. _blend_visible_portion's own docstring already records that the scipy path it replaced was dead; the replacement landed but the corpse stayed. 2. scripts/check_plugin.py: also search ../ledmatrix-plugins/plugins. The scoreboards live in the sibling checkout, so --all silently skipped every one of them and only --plugin-dir reached them. 3. .gitignore: ignore config/.config_secrets.json.tmp.*, which the suite leaves behind several of per run. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ __pycache__/
|
||||
|
||||
# Secrets
|
||||
config/config_secrets.json
|
||||
# Atomic writes leave these behind when a save or a test is interrupted;
|
||||
# the suite drops several per run.
|
||||
config/.config_secrets.json.tmp.*
|
||||
config/config.json
|
||||
config/config.json.backup
|
||||
config/wifi_config.json
|
||||
|
||||
@@ -78,6 +78,9 @@ logger = get_logger("[Check Plugin]")
|
||||
DEFAULT_SEARCH_DIRS = [
|
||||
str(PROJECT_ROOT / 'plugins'),
|
||||
str(PROJECT_ROOT / 'plugin-repos'),
|
||||
# The scoreboards live in the sibling ledmatrix-plugins checkout, not
|
||||
# in this repo. Without this, --all silently skips every one of them.
|
||||
str(PROJECT_ROOT.parent / 'ledmatrix-plugins' / 'plugins'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -534,168 +534,6 @@ class ScrollHelper:
|
||||
|
||||
return Image.frombytes('RGB', _size, self._frame_buffer.tobytes())
|
||||
|
||||
def _get_visible_portion_subpixel(self, start_x_int: int, fractional: float) -> Image.Image:
|
||||
"""
|
||||
Get visible portion with sub-pixel interpolation for smooth scrolling.
|
||||
Uses bilinear interpolation to blend between pixels.
|
||||
"""
|
||||
# We need to extract a region that's 1 pixel wider to allow for interpolation
|
||||
start_x = start_x_int
|
||||
end_x = start_x_int + self.display_width + 1
|
||||
|
||||
# Check if we need wrap-around
|
||||
if end_x <= self.cached_image.width:
|
||||
# Normal case: extract region with 1 extra pixel for interpolation
|
||||
source_region = self.cached_array[:, start_x:end_x]
|
||||
|
||||
# Use bilinear interpolation for sub-pixel shifting
|
||||
if HAS_SCIPY:
|
||||
# Use scipy for high-quality sub-pixel shifting
|
||||
shifted = shift(source_region, (0, -fractional, 0), mode='nearest', order=1, prefilter=False)
|
||||
# Extract the display_width portion
|
||||
frame_array = shifted[:, :self.display_width].astype(np.uint8)
|
||||
else:
|
||||
# Fallback: simple linear interpolation using numpy
|
||||
# Blend between current and next pixel based on fractional part
|
||||
frame_array = self._interpolate_subpixel(source_region, fractional)
|
||||
|
||||
return Image.fromarray(frame_array)
|
||||
else:
|
||||
# Wrap-around case with sub-pixel
|
||||
# Use pre-allocated buffer
|
||||
if self._frame_buffer is None or self._frame_buffer.shape != (self.display_height, self.display_width, 3):
|
||||
self._frame_buffer = np.zeros((self.display_height, self.display_width, 3), dtype=np.uint8)
|
||||
|
||||
width1 = self.cached_image.width - start_x
|
||||
if width1 > 0:
|
||||
# First part from end of image
|
||||
# Need width1 + 1 pixels for interpolation
|
||||
source1_width = min(width1 + 1, self.cached_image.width - start_x)
|
||||
source1 = self.cached_array[:, start_x:start_x + source1_width]
|
||||
if HAS_SCIPY:
|
||||
shifted1 = shift(source1, (0, -fractional, 0), mode='nearest', order=1, prefilter=False)
|
||||
# Ensure we get exactly width1 pixels, padding if necessary
|
||||
if shifted1.shape[1] >= width1:
|
||||
self._frame_buffer[:, :width1] = shifted1[:, :width1].astype(np.uint8)
|
||||
else:
|
||||
# Shifted array is smaller - pad with zeros or repeat last pixel
|
||||
actual_width = shifted1.shape[1]
|
||||
self._frame_buffer[:, :actual_width] = shifted1.astype(np.uint8)
|
||||
if actual_width < width1:
|
||||
# Pad with last pixel
|
||||
self._frame_buffer[:, actual_width:width1] = shifted1[:, -1:].astype(np.uint8)
|
||||
else:
|
||||
interpolated1 = self._interpolate_subpixel(source1, fractional, output_width=width1)
|
||||
# Ensure exact width match
|
||||
if interpolated1.shape[1] == width1:
|
||||
self._frame_buffer[:, :width1] = interpolated1
|
||||
else:
|
||||
# Handle size mismatch
|
||||
copy_width = min(width1, interpolated1.shape[1])
|
||||
self._frame_buffer[:, :copy_width] = interpolated1[:, :copy_width]
|
||||
if copy_width < width1:
|
||||
self._frame_buffer[:, copy_width:width1] = interpolated1[:, -1:]
|
||||
|
||||
# Second part from beginning
|
||||
remaining_width = self.display_width - width1
|
||||
if remaining_width > 0:
|
||||
source2 = self.cached_array[:, :remaining_width + 1]
|
||||
if HAS_SCIPY:
|
||||
shifted2 = shift(source2, (0, -fractional, 0), mode='nearest', order=1, prefilter=False)
|
||||
# Ensure we get exactly remaining_width pixels
|
||||
if shifted2.shape[1] >= remaining_width:
|
||||
self._frame_buffer[:, width1:width1 + remaining_width] = shifted2[:, :remaining_width].astype(np.uint8)
|
||||
else:
|
||||
# Shifted array is smaller - pad if necessary
|
||||
actual_width = shifted2.shape[1]
|
||||
self._frame_buffer[:, width1:width1 + actual_width] = shifted2.astype(np.uint8)
|
||||
if actual_width < remaining_width:
|
||||
self._frame_buffer[:, width1 + actual_width:width1 + remaining_width] = shifted2[:, -1:].astype(np.uint8)
|
||||
else:
|
||||
interpolated2 = self._interpolate_subpixel(source2, fractional, output_width=remaining_width)
|
||||
# Ensure exact width match
|
||||
if interpolated2.shape[1] == remaining_width:
|
||||
self._frame_buffer[:, width1:] = interpolated2
|
||||
else:
|
||||
copy_width = min(remaining_width, interpolated2.shape[1])
|
||||
self._frame_buffer[:, width1:width1 + copy_width] = interpolated2[:, :copy_width]
|
||||
if copy_width < remaining_width:
|
||||
self._frame_buffer[:, width1 + copy_width:width1 + remaining_width] = interpolated2[:, -1:]
|
||||
else:
|
||||
# Edge case: wrap to beginning
|
||||
source = self.cached_array[:, :self.display_width + 1]
|
||||
if HAS_SCIPY:
|
||||
shifted = shift(source, (0, -fractional, 0), mode='nearest', order=1, prefilter=False)
|
||||
# Ensure we get exactly display_width pixels
|
||||
if shifted.shape[1] >= self.display_width:
|
||||
self._frame_buffer = shifted[:, :self.display_width].astype(np.uint8)
|
||||
else:
|
||||
# Shifted array is smaller - pad if necessary
|
||||
actual_width = shifted.shape[1]
|
||||
self._frame_buffer[:, :actual_width] = shifted.astype(np.uint8)
|
||||
if actual_width < self.display_width:
|
||||
self._frame_buffer[:, actual_width:] = shifted[:, -1:].astype(np.uint8)
|
||||
else:
|
||||
interpolated = self._interpolate_subpixel(source, fractional, output_width=self.display_width)
|
||||
# _interpolate_subpixel now always returns exact width, so this should work
|
||||
self._frame_buffer = interpolated
|
||||
|
||||
return Image.fromarray(self._frame_buffer)
|
||||
|
||||
def _interpolate_subpixel(self, source: np.ndarray, fractional: float, output_width: Optional[int] = None) -> np.ndarray:
|
||||
"""
|
||||
Simple linear interpolation for sub-pixel positioning.
|
||||
Blends between adjacent pixels based on fractional offset.
|
||||
|
||||
Args:
|
||||
source: Source array to interpolate (width should be at least output_width + 1)
|
||||
fractional: Fractional part of scroll position (0.0-1.0)
|
||||
output_width: Desired output width (defaults to display_width)
|
||||
|
||||
Returns:
|
||||
Interpolated array of shape (height, output_width, 3) - ALWAYS exactly output_width
|
||||
"""
|
||||
if output_width is None:
|
||||
output_width = self.display_width
|
||||
|
||||
# Always return exactly output_width pixels, padding if necessary
|
||||
result = np.zeros((source.shape[0], output_width, 3), dtype=np.uint8)
|
||||
|
||||
# Ensure we have enough source pixels for interpolation
|
||||
if source.shape[1] < 2:
|
||||
# Very small source - just copy what we have and pad
|
||||
copy_width = min(source.shape[1], output_width)
|
||||
result[:, :copy_width] = source[:, :copy_width].astype(np.uint8)
|
||||
if copy_width < output_width:
|
||||
# Pad with last pixel
|
||||
result[:, copy_width:] = source[:, -1:].astype(np.uint8)
|
||||
return result
|
||||
|
||||
# Calculate how many pixels we can actually interpolate
|
||||
# Need at least 2 pixels to interpolate, so max output is source.shape[1] - 1
|
||||
max_interpolated_width = source.shape[1] - 1
|
||||
interpolated_width = min(output_width, max_interpolated_width)
|
||||
|
||||
if interpolated_width > 0:
|
||||
# Extract pixels at x and x+1 for interpolation
|
||||
pixels_x = source[:, :interpolated_width].astype(np.float32)
|
||||
pixels_x1 = source[:, 1:interpolated_width + 1].astype(np.float32)
|
||||
|
||||
# Linear interpolation
|
||||
interpolated = pixels_x * (1.0 - fractional) + pixels_x1 * fractional
|
||||
|
||||
# Clip and convert back to uint8
|
||||
interpolated = np.clip(interpolated, 0, 255).astype(np.uint8)
|
||||
|
||||
# Copy interpolated portion to result
|
||||
result[:, :interpolated_width] = interpolated
|
||||
|
||||
# If we need more pixels than we can interpolate, pad with last pixel
|
||||
if interpolated_width < output_width:
|
||||
result[:, interpolated_width:] = source[:, -1:].astype(np.uint8)
|
||||
|
||||
return result
|
||||
|
||||
def calculate_dynamic_duration(self) -> int:
|
||||
"""
|
||||
Calculate display duration based on content width and scroll settings.
|
||||
|
||||
Reference in New Issue
Block a user