Add skip LEDs feature with physical resampling and per-edge tick labels
Skip LEDs at the start/end of the strip are blacked out while the full screen perimeter is resampled onto the remaining active LEDs using linear interpolation. Calibration canvas tick labels show per-edge display ranges clipped to the active LED range. Moved LED offset control from inline overlay to a dedicated form row alongside the new skip inputs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,9 @@ class CalibrationConfig:
|
||||
span_bottom_end: float = 1.0
|
||||
span_left_start: float = 0.0
|
||||
span_left_end: float = 1.0
|
||||
# Skip LEDs: black out N LEDs at the start/end of the strip
|
||||
skip_leds_start: int = 0
|
||||
skip_leds_end: int = 0
|
||||
|
||||
def build_segments(self) -> List[CalibrationSegment]:
|
||||
"""Derive segment list from core parameters."""
|
||||
@@ -263,8 +266,12 @@ class PixelMapper:
|
||||
ValueError: If border pixels don't match calibration
|
||||
"""
|
||||
total_leds = self.calibration.get_total_leds()
|
||||
skip_start = self.calibration.skip_leds_start
|
||||
skip_end = self.calibration.skip_leds_end
|
||||
active_count = max(0, total_leds - skip_start - skip_end)
|
||||
use_fast_avg = self.interpolation_mode == "average"
|
||||
|
||||
# Phase 1: Map full perimeter to total_leds positions
|
||||
if use_fast_avg:
|
||||
led_array = np.zeros((total_leds, 3), dtype=np.uint8)
|
||||
else:
|
||||
@@ -304,16 +311,51 @@ class PixelMapper:
|
||||
color = self._calc_color(pixel_segment)
|
||||
led_colors[led_idx] = color
|
||||
|
||||
# Phase 2: Offset rotation
|
||||
offset = self.calibration.offset % total_leds if total_leds > 0 else 0
|
||||
|
||||
if use_fast_avg:
|
||||
if offset > 0:
|
||||
led_array = np.roll(led_array, offset, axis=0)
|
||||
|
||||
# Phase 3: Physical skip — resample full perimeter to active LEDs
|
||||
# Maps the entire screen to active_count positions so each active LED
|
||||
# covers a proportionally larger slice of the perimeter.
|
||||
if active_count > 0 and active_count < total_leds:
|
||||
src = np.linspace(0, total_leds - 1, active_count)
|
||||
full_f = led_array.astype(np.float64)
|
||||
x = np.arange(total_leds, dtype=np.float64)
|
||||
resampled = np.empty((active_count, 3), dtype=np.uint8)
|
||||
for ch in range(3):
|
||||
resampled[:, ch] = np.round(
|
||||
np.interp(src, x, full_f[:, ch])
|
||||
).astype(np.uint8)
|
||||
led_array[:] = 0
|
||||
end_idx = total_leds - skip_end
|
||||
led_array[skip_start:end_idx] = resampled
|
||||
elif active_count <= 0:
|
||||
led_array[:] = 0
|
||||
|
||||
return [tuple(c) for c in led_array]
|
||||
else:
|
||||
if offset > 0:
|
||||
led_colors = led_colors[total_leds - offset:] + led_colors[:total_leds - offset]
|
||||
logger.debug(f"Mapped border pixels to {total_leds} LED colors (offset={offset})")
|
||||
|
||||
# Phase 3: Physical skip — resample full perimeter to active LEDs
|
||||
if active_count > 0 and active_count < total_leds:
|
||||
arr = np.array(led_colors, dtype=np.float64)
|
||||
src = np.linspace(0, total_leds - 1, active_count)
|
||||
x = np.arange(total_leds, dtype=np.float64)
|
||||
resampled = np.empty((active_count, 3), dtype=np.float64)
|
||||
for ch in range(3):
|
||||
resampled[:, ch] = np.interp(src, x, arr[:, ch])
|
||||
led_colors = [(0, 0, 0)] * total_leds
|
||||
for i in range(active_count):
|
||||
r, g, b = resampled[i]
|
||||
led_colors[skip_start + i] = (int(round(r)), int(round(g)), int(round(b)))
|
||||
elif active_count <= 0:
|
||||
led_colors = [(0, 0, 0)] * total_leds
|
||||
|
||||
return led_colors
|
||||
|
||||
def test_calibration(self, edge: str, color: Tuple[int, int, int]) -> List[Tuple[int, int, int]]:
|
||||
@@ -419,6 +461,8 @@ def calibration_from_dict(data: dict) -> CalibrationConfig:
|
||||
span_bottom_end=data.get("span_bottom_end", 1.0),
|
||||
span_left_start=data.get("span_left_start", 0.0),
|
||||
span_left_end=data.get("span_left_end", 1.0),
|
||||
skip_leds_start=data.get("skip_leds_start", 0),
|
||||
skip_leds_end=data.get("skip_leds_end", 0),
|
||||
)
|
||||
|
||||
config.validate()
|
||||
@@ -457,4 +501,9 @@ def calibration_to_dict(config: CalibrationConfig) -> dict:
|
||||
if start != 0.0 or end != 1.0:
|
||||
result[f"span_{edge}_start"] = start
|
||||
result[f"span_{edge}_end"] = end
|
||||
# Include skip fields only when non-default
|
||||
if config.skip_leds_start > 0:
|
||||
result["skip_leds_start"] = config.skip_leds_start
|
||||
if config.skip_leds_end > 0:
|
||||
result["skip_leds_end"] = config.skip_leds_end
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user