Add partial LED side coverage (edge spans) for calibration
Some checks failed
Validate / validate (push) Failing after 8s

Allow LEDs to cover only a fraction of each screen edge via draggable
span bars in the calibration UI. Per-edge start/end (0.0-1.0) values
control which portion of the screen border is sampled for LED colors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 02:28:36 +03:00
parent 01114e125e
commit 2b953e2e3e
5 changed files with 338 additions and 17 deletions

View File

@@ -65,6 +65,15 @@ class CalibrationConfig:
leds_right: int = 0
leds_bottom: int = 0
leds_left: int = 0
# Per-edge span: fraction of screen side covered by LEDs (0.01.0)
span_top_start: float = 0.0
span_top_end: float = 1.0
span_right_start: float = 0.0
span_right_end: float = 1.0
span_bottom_start: float = 0.0
span_bottom_end: float = 1.0
span_left_start: float = 0.0
span_left_end: float = 1.0
def build_segments(self) -> List[CalibrationSegment]:
"""Derive segment list from core parameters."""
@@ -99,6 +108,13 @@ class CalibrationConfig:
"""Get derived segment list."""
return self.build_segments()
def get_edge_span(self, edge: str) -> tuple[float, float]:
"""Get span (start, end) for a given edge."""
return (
getattr(self, f"span_{edge}_start", 0.0),
getattr(self, f"span_{edge}_end", 1.0),
)
def validate(self) -> bool:
"""Validate calibration configuration.
@@ -117,6 +133,13 @@ class CalibrationConfig:
if count < 0:
raise ValueError(f"LED count for {edge} must be non-negative, got {count}")
for edge in ["top", "right", "bottom", "left"]:
start, end = self.get_edge_span(edge)
if not (0.0 <= start <= 1.0) or not (0.0 <= end <= 1.0):
raise ValueError(f"Span for {edge} must be in [0.0, 1.0], got ({start}, {end})")
if end <= start:
raise ValueError(f"Span end must be greater than start for {edge}, got ({start}, {end})")
return True
def get_total_leds(self) -> int:
@@ -202,6 +225,20 @@ class PixelMapper:
else: # left
edge_pixels = border_pixels.left
# Slice to span region if not full coverage
span_start, span_end = self.calibration.get_edge_span(edge_name)
if span_start > 0.0 or span_end < 1.0:
if edge_name in ("top", "bottom"):
total_w = edge_pixels.shape[1]
s = int(span_start * total_w)
e = int(span_end * total_w)
edge_pixels = edge_pixels[:, s:e, :]
else:
total_h = edge_pixels.shape[0]
s = int(span_start * total_h)
e = int(span_end * total_h)
edge_pixels = edge_pixels[s:e, :, :]
# Divide edge into segments matching LED count
try:
pixel_segments = get_edge_segments(
@@ -333,6 +370,14 @@ def calibration_from_dict(data: dict) -> CalibrationConfig:
leds_right=data.get("leds_right", 0),
leds_bottom=data.get("leds_bottom", 0),
leds_left=data.get("leds_left", 0),
span_top_start=data.get("span_top_start", 0.0),
span_top_end=data.get("span_top_end", 1.0),
span_right_start=data.get("span_right_start", 0.0),
span_right_end=data.get("span_right_end", 1.0),
span_bottom_start=data.get("span_bottom_start", 0.0),
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),
)
config.validate()
@@ -355,7 +400,7 @@ def calibration_to_dict(config: CalibrationConfig) -> dict:
Returns:
Dictionary representation
"""
return {
result = {
"layout": config.layout,
"start_position": config.start_position,
"offset": config.offset,
@@ -364,3 +409,11 @@ def calibration_to_dict(config: CalibrationConfig) -> dict:
"leds_bottom": config.leds_bottom,
"leds_left": config.leds_left,
}
# Include span fields only when not default (full coverage)
for edge in ["top", "right", "bottom", "left"]:
start = getattr(config, f"span_{edge}_start")
end = getattr(config, f"span_{edge}_end")
if start != 0.0 or end != 1.0:
result[f"span_{edge}_start"] = start
result[f"span_{edge}_end"] = end
return result