CSS: add led_count field; calibration dialog improvements; color corrections collapsible section
- Add explicit led_count to PictureColorStripSource (0 = auto from calibration) - Stream pads with black or truncates to match led_count exactly - Calibration dialog: show led_count input above visual editor in CSS mode - Calibration dialog: pre-populate led_count with effective count (cal sum) when stored value is 0 - Calibration dialog: sync preview label live as led_count input changes - CSS editor: group brightness/saturation/gamma into collapsible "Color Corrections" section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ def _css_to_response(source) -> ColorStripSourceResponse:
|
|||||||
gamma=getattr(source, "gamma", None),
|
gamma=getattr(source, "gamma", None),
|
||||||
smoothing=getattr(source, "smoothing", None),
|
smoothing=getattr(source, "smoothing", None),
|
||||||
interpolation_mode=getattr(source, "interpolation_mode", None),
|
interpolation_mode=getattr(source, "interpolation_mode", None),
|
||||||
|
led_count=getattr(source, "led_count", 0),
|
||||||
calibration=calibration,
|
calibration=calibration,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
@@ -93,6 +94,7 @@ async def create_color_strip_source(
|
|||||||
gamma=data.gamma,
|
gamma=data.gamma,
|
||||||
smoothing=data.smoothing,
|
smoothing=data.smoothing,
|
||||||
interpolation_mode=data.interpolation_mode,
|
interpolation_mode=data.interpolation_mode,
|
||||||
|
led_count=data.led_count,
|
||||||
calibration=calibration,
|
calibration=calibration,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
)
|
)
|
||||||
@@ -143,6 +145,7 @@ async def update_color_strip_source(
|
|||||||
gamma=data.gamma,
|
gamma=data.gamma,
|
||||||
smoothing=data.smoothing,
|
smoothing=data.smoothing,
|
||||||
interpolation_mode=data.interpolation_mode,
|
interpolation_mode=data.interpolation_mode,
|
||||||
|
led_count=data.led_count,
|
||||||
calibration=calibration,
|
calibration=calibration,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ColorStripSourceCreate(BaseModel):
|
|||||||
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
|
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
|
||||||
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||||
|
led_count: int = Field(default=0, description="Total LED count (0 = auto from calibration)", ge=0)
|
||||||
calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)")
|
calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ class ColorStripSourceUpdate(BaseModel):
|
|||||||
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
|
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
|
||||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)")
|
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)")
|
||||||
|
led_count: Optional[int] = Field(None, description="Total LED count (0 = auto from calibration)", ge=0)
|
||||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ class ColorStripSourceResponse(BaseModel):
|
|||||||
gamma: Optional[float] = Field(None, description="Gamma correction")
|
gamma: Optional[float] = Field(None, description="Gamma correction")
|
||||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing")
|
smoothing: Optional[float] = Field(None, description="Temporal smoothing")
|
||||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||||
|
led_count: int = Field(0, description="Total LED count (0 = auto from calibration)")
|
||||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
self._pixel_mapper = PixelMapper(
|
self._pixel_mapper = PixelMapper(
|
||||||
self._calibration, interpolation_mode=self._interpolation_mode
|
self._calibration, interpolation_mode=self._interpolation_mode
|
||||||
)
|
)
|
||||||
self._led_count: int = self._calibration.get_total_leds()
|
cal_leds = self._calibration.get_total_leds()
|
||||||
|
self._led_count: int = source.led_count if source.led_count > 0 else cal_leds
|
||||||
self._gamma_lut: np.ndarray = _build_gamma_lut(self._gamma)
|
self._gamma_lut: np.ndarray = _build_gamma_lut(self._gamma)
|
||||||
|
|
||||||
# Thread-safe color cache
|
# Thread-safe color cache
|
||||||
@@ -224,10 +225,12 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
if (
|
if (
|
||||||
source.interpolation_mode != self._interpolation_mode
|
source.interpolation_mode != self._interpolation_mode
|
||||||
or source.calibration != self._calibration
|
or source.calibration != self._calibration
|
||||||
|
or source.led_count != self._led_count
|
||||||
):
|
):
|
||||||
self._interpolation_mode = source.interpolation_mode
|
self._interpolation_mode = source.interpolation_mode
|
||||||
self._calibration = source.calibration
|
self._calibration = source.calibration
|
||||||
self._led_count = source.calibration.get_total_leds()
|
cal_leds = source.calibration.get_total_leds()
|
||||||
|
self._led_count = source.led_count if source.led_count > 0 else cal_leds
|
||||||
self._pixel_mapper = PixelMapper(
|
self._pixel_mapper = PixelMapper(
|
||||||
source.calibration, interpolation_mode=source.interpolation_mode
|
source.calibration, interpolation_mode=source.interpolation_mode
|
||||||
)
|
)
|
||||||
@@ -263,6 +266,15 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
led_colors = self._pixel_mapper.map_border_to_leds(border_pixels)
|
led_colors = self._pixel_mapper.map_border_to_leds(border_pixels)
|
||||||
t2 = time.perf_counter()
|
t2 = time.perf_counter()
|
||||||
|
|
||||||
|
# Pad or truncate to match the declared led_count
|
||||||
|
target_count = self._led_count
|
||||||
|
if target_count > 0 and len(led_colors) != target_count:
|
||||||
|
if len(led_colors) < target_count:
|
||||||
|
pad = np.zeros((target_count - len(led_colors), 3), dtype=np.uint8)
|
||||||
|
led_colors = np.concatenate([led_colors, pad])
|
||||||
|
else:
|
||||||
|
led_colors = led_colors[:target_count]
|
||||||
|
|
||||||
# Temporal smoothing
|
# Temporal smoothing
|
||||||
smoothing = self._smoothing
|
smoothing = self._smoothing
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
color: #FFC107;
|
color: #FFC107;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.inputs-dimmed .edge-led-input {
|
.inputs-dimmed .edge-led-input {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -221,6 +221,47 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-collapse {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-collapse > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-collapse > summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
.form-collapse > summary::before {
|
||||||
|
content: '▶';
|
||||||
|
font-size: 0.6rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-collapse[open] > summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-collapse > summary:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-collapse-body {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background: rgba(244, 67, 54, 0.1);
|
background: rgba(244, 67, 54, 0.1);
|
||||||
border: 1px solid var(--danger-color);
|
border: 1px solid var(--danger-color);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class CalibrationModal extends Modal {
|
|||||||
skip_start: this.$('cal-skip-start').value,
|
skip_start: this.$('cal-skip-start').value,
|
||||||
skip_end: this.$('cal-skip-end').value,
|
skip_end: this.$('cal-skip-end').value,
|
||||||
border_width: this.$('cal-border-width').value,
|
border_width: this.$('cal-border-width').value,
|
||||||
|
led_count: this.$('cal-css-led-count').value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ export async function showCalibration(deviceId) {
|
|||||||
|
|
||||||
document.getElementById('calibration-device-id').value = device.id;
|
document.getElementById('calibration-device-id').value = device.id;
|
||||||
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
|
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
|
||||||
|
document.getElementById('cal-css-led-count-group').style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('cal-start-position').value = calibration.start_position;
|
document.getElementById('cal-start-position').value = calibration.start_position;
|
||||||
document.getElementById('cal-layout').value = calibration.layout;
|
document.getElementById('cal-layout').value = calibration.layout;
|
||||||
@@ -216,6 +218,11 @@ export async function showCSSCalibration(cssId) {
|
|||||||
const preview = document.querySelector('.calibration-preview');
|
const preview = document.querySelector('.calibration-preview');
|
||||||
preview.style.aspectRatio = '';
|
preview.style.aspectRatio = '';
|
||||||
document.getElementById('cal-device-led-count-inline').textContent = '—';
|
document.getElementById('cal-device-led-count-inline').textContent = '—';
|
||||||
|
const ledCountGroup = document.getElementById('cal-css-led-count-group');
|
||||||
|
ledCountGroup.style.display = '';
|
||||||
|
const calLeds = (calibration.leds_top || 0) + (calibration.leds_right || 0) +
|
||||||
|
(calibration.leds_bottom || 0) + (calibration.leds_left || 0);
|
||||||
|
document.getElementById('cal-css-led-count').value = source.led_count || calLeds || 0;
|
||||||
|
|
||||||
document.getElementById('cal-start-position').value = calibration.start_position || 'bottom_left';
|
document.getElementById('cal-start-position').value = calibration.start_position || 'bottom_left';
|
||||||
document.getElementById('cal-layout').value = calibration.layout || 'clockwise';
|
document.getElementById('cal-layout').value = calibration.layout || 'clockwise';
|
||||||
@@ -286,8 +293,17 @@ export function updateCalibrationPreview() {
|
|||||||
parseInt(document.getElementById('cal-left-leds').value || 0);
|
parseInt(document.getElementById('cal-left-leds').value || 0);
|
||||||
const totalEl = document.querySelector('.preview-screen-total');
|
const totalEl = document.querySelector('.preview-screen-total');
|
||||||
const inCSS = _isCSS();
|
const inCSS = _isCSS();
|
||||||
const deviceCount = inCSS ? null : parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
|
const declaredCount = inCSS
|
||||||
const mismatch = !inCSS && total !== deviceCount;
|
? parseInt(document.getElementById('cal-css-led-count').value || 0)
|
||||||
|
: parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
|
||||||
|
if (inCSS) {
|
||||||
|
document.getElementById('cal-device-led-count-inline').textContent = declaredCount || '—';
|
||||||
|
}
|
||||||
|
// In device mode: calibration total must exactly equal device LED count
|
||||||
|
// In CSS mode: warn only if calibrated LEDs exceed the declared total (padding handles the rest)
|
||||||
|
const mismatch = inCSS
|
||||||
|
? (declaredCount > 0 && total > declaredCount)
|
||||||
|
: (total !== declaredCount);
|
||||||
document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total;
|
document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total;
|
||||||
if (totalEl) totalEl.classList.toggle('mismatch', mismatch);
|
if (totalEl) totalEl.classList.toggle('mismatch', mismatch);
|
||||||
|
|
||||||
@@ -831,10 +847,18 @@ export async function saveCalibration() {
|
|||||||
const leftLeds = parseInt(document.getElementById('cal-left-leds').value || 0);
|
const leftLeds = parseInt(document.getElementById('cal-left-leds').value || 0);
|
||||||
const total = topLeds + rightLeds + bottomLeds + leftLeds;
|
const total = topLeds + rightLeds + bottomLeds + leftLeds;
|
||||||
|
|
||||||
|
const declaredLedCount = cssMode
|
||||||
|
? parseInt(document.getElementById('cal-css-led-count').value) || 0
|
||||||
|
: parseInt(document.getElementById('cal-device-led-count-inline').textContent) || 0;
|
||||||
if (!cssMode) {
|
if (!cssMode) {
|
||||||
const deviceLedCount = parseInt(document.getElementById('cal-device-led-count-inline').textContent);
|
if (total !== declaredLedCount) {
|
||||||
if (total !== deviceLedCount) {
|
error.textContent = `Total LEDs (${total}) must equal device LED count (${declaredLedCount})`;
|
||||||
error.textContent = `Total LEDs (${total}) must equal device LED count (${deviceLedCount})`;
|
error.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (declaredLedCount > 0 && total > declaredLedCount) {
|
||||||
|
error.textContent = `Calibrated LEDs (${total}) exceed total LED count (${declaredLedCount})`;
|
||||||
error.style.display = 'block';
|
error.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -862,7 +886,7 @@ export async function saveCalibration() {
|
|||||||
if (cssMode) {
|
if (cssMode) {
|
||||||
response = await fetchWithAuth(`/color-strip-sources/${cssId}`, {
|
response = await fetchWithAuth(`/color-strip-sources/${cssId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ calibration }),
|
body: JSON.stringify({ calibration, led_count: declaredLedCount }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {
|
response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class CSSEditorModal extends Modal {
|
|||||||
brightness: document.getElementById('css-editor-brightness').value,
|
brightness: document.getElementById('css-editor-brightness').value,
|
||||||
saturation: document.getElementById('css-editor-saturation').value,
|
saturation: document.getElementById('css-editor-saturation').value,
|
||||||
gamma: document.getElementById('css-editor-gamma').value,
|
gamma: document.getElementById('css-editor-gamma').value,
|
||||||
|
led_count: document.getElementById('css-editor-led-count').value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,8 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
? pictureSourceMap[source.picture_source_id].name
|
? pictureSourceMap[source.picture_source_id].name
|
||||||
: source.picture_source_id || '—';
|
: source.picture_source_id || '—';
|
||||||
const cal = source.calibration || {};
|
const cal = source.calibration || {};
|
||||||
const ledCount = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
||||||
|
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card" data-css-id="${source.id}">
|
<div class="card" data-css-id="${source.id}">
|
||||||
@@ -107,6 +109,8 @@ export async function showCSSEditor(cssId = null) {
|
|||||||
document.getElementById('css-editor-gamma').value = gamma;
|
document.getElementById('css-editor-gamma').value = gamma;
|
||||||
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
|
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
|
||||||
|
|
||||||
|
document.getElementById('css-editor-led-count').value = css.led_count ?? 0;
|
||||||
|
|
||||||
document.getElementById('css-editor-title').textContent = t('color_strip.edit');
|
document.getElementById('css-editor-title').textContent = t('color_strip.edit');
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('css-editor-id').value = '';
|
document.getElementById('css-editor-id').value = '';
|
||||||
@@ -122,6 +126,7 @@ export async function showCSSEditor(cssId = null) {
|
|||||||
document.getElementById('css-editor-saturation-value').textContent = '1.00';
|
document.getElementById('css-editor-saturation-value').textContent = '1.00';
|
||||||
document.getElementById('css-editor-gamma').value = 1.0;
|
document.getElementById('css-editor-gamma').value = 1.0;
|
||||||
document.getElementById('css-editor-gamma-value').textContent = '1.00';
|
document.getElementById('css-editor-gamma-value').textContent = '1.00';
|
||||||
|
document.getElementById('css-editor-led-count').value = 0;
|
||||||
document.getElementById('css-editor-title').textContent = t('color_strip.add');
|
document.getElementById('css-editor-title').textContent = t('color_strip.add');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +165,7 @@ export async function saveCSSEditor() {
|
|||||||
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
||||||
saturation: parseFloat(document.getElementById('css-editor-saturation').value),
|
saturation: parseFloat(document.getElementById('css-editor-saturation').value),
|
||||||
gamma: parseFloat(document.getElementById('css-editor-gamma').value),
|
gamma: parseFloat(document.getElementById('css-editor-gamma').value),
|
||||||
|
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ export function createDeviceCard(device) {
|
|||||||
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||||
⚙️
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showCalibration('${device.id}')" title="${t('device.button.calibrate')}">
|
|
||||||
📐
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
"color_strip.interpolation.dominant": "Dominant",
|
"color_strip.interpolation.dominant": "Dominant",
|
||||||
"color_strip.smoothing": "Smoothing:",
|
"color_strip.smoothing": "Smoothing:",
|
||||||
"color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
"color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
||||||
|
"color_strip.color_corrections": "Color Corrections",
|
||||||
"color_strip.brightness": "Brightness:",
|
"color_strip.brightness": "Brightness:",
|
||||||
"color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.",
|
"color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.",
|
||||||
"color_strip.saturation": "Saturation:",
|
"color_strip.saturation": "Saturation:",
|
||||||
@@ -561,6 +562,8 @@
|
|||||||
"color_strip.test_device": "Test on Device:",
|
"color_strip.test_device": "Test on Device:",
|
||||||
"color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles",
|
"color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles",
|
||||||
"color_strip.leds": "LED count",
|
"color_strip.leds": "LED count",
|
||||||
|
"color_strip.led_count": "LED Count:",
|
||||||
|
"color_strip.led_count.hint": "Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. Useful when the strip has LEDs behind the TV that are not mapped to screen edges — those LEDs will be sent black.",
|
||||||
"color_strip.created": "Color strip source created",
|
"color_strip.created": "Color strip source created",
|
||||||
"color_strip.updated": "Color strip source updated",
|
"color_strip.updated": "Color strip source updated",
|
||||||
"color_strip.deleted": "Color strip source deleted",
|
"color_strip.deleted": "Color strip source deleted",
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
"color_strip.interpolation.dominant": "Доминирующий",
|
"color_strip.interpolation.dominant": "Доминирующий",
|
||||||
"color_strip.smoothing": "Сглаживание:",
|
"color_strip.smoothing": "Сглаживание:",
|
||||||
"color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.",
|
"color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.",
|
||||||
|
"color_strip.color_corrections": "Цветокоррекция",
|
||||||
"color_strip.brightness": "Яркость:",
|
"color_strip.brightness": "Яркость:",
|
||||||
"color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.",
|
"color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.",
|
||||||
"color_strip.saturation": "Насыщенность:",
|
"color_strip.saturation": "Насыщенность:",
|
||||||
@@ -561,6 +562,8 @@
|
|||||||
"color_strip.test_device": "Тестировать на устройстве:",
|
"color_strip.test_device": "Тестировать на устройстве:",
|
||||||
"color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку",
|
"color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку",
|
||||||
"color_strip.leds": "Количество светодиодов",
|
"color_strip.leds": "Количество светодиодов",
|
||||||
|
"color_strip.led_count": "Количество LED:",
|
||||||
|
"color_strip.led_count.hint": "Общее число светодиодов на физической полосе. 0 = взять из калибровки. Укажите явно, если на полосе есть светодиоды за телевизором, не привязанные к краям экрана — им будет отправлен чёрный цвет.",
|
||||||
"color_strip.created": "Источник цветовой полосы создан",
|
"color_strip.created": "Источник цветовой полосы создан",
|
||||||
"color_strip.updated": "Источник цветовой полосы обновлён",
|
"color_strip.updated": "Источник цветовой полосы обновлён",
|
||||||
"color_strip.deleted": "Источник цветовой полосы удалён",
|
"color_strip.deleted": "Источник цветовой полосы удалён",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from wled_controller.core.capture.calibration import (
|
|||||||
CalibrationConfig,
|
CalibrationConfig,
|
||||||
calibration_from_dict,
|
calibration_from_dict,
|
||||||
calibration_to_dict,
|
calibration_to_dict,
|
||||||
create_default_calibration,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -53,6 +52,7 @@ class ColorStripSource:
|
|||||||
"smoothing": None,
|
"smoothing": None,
|
||||||
"interpolation_mode": None,
|
"interpolation_mode": None,
|
||||||
"calibration": None,
|
"calibration": None,
|
||||||
|
"led_count": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -82,7 +82,7 @@ class ColorStripSource:
|
|||||||
calibration = (
|
calibration = (
|
||||||
calibration_from_dict(calibration_data)
|
calibration_from_dict(calibration_data)
|
||||||
if calibration_data
|
if calibration_data
|
||||||
else create_default_calibration(0)
|
else CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only "picture" type for now; extend with elif branches for future types
|
# Only "picture" type for now; extend with elif branches for future types
|
||||||
@@ -97,6 +97,7 @@ class ColorStripSource:
|
|||||||
smoothing=data["smoothing"] if data.get("smoothing") is not None else 0.3,
|
smoothing=data["smoothing"] if data.get("smoothing") is not None else 0.3,
|
||||||
interpolation_mode=data.get("interpolation_mode") or "average",
|
interpolation_mode=data.get("interpolation_mode") or "average",
|
||||||
calibration=calibration,
|
calibration=calibration,
|
||||||
|
led_count=data.get("led_count") or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,7 +116,10 @@ class PictureColorStripSource(ColorStripSource):
|
|||||||
gamma: float = 1.0 # 1.0 = no correction; <1 = brighter, >1 = darker mids
|
gamma: float = 1.0 # 1.0 = no correction; <1 = brighter, >1 = darker mids
|
||||||
smoothing: float = 0.3 # temporal smoothing (0.0 = none, 1.0 = full)
|
smoothing: float = 0.3 # temporal smoothing (0.0 = none, 1.0 = full)
|
||||||
interpolation_mode: str = "average" # "average" | "median" | "dominant"
|
interpolation_mode: str = "average" # "average" | "median" | "dominant"
|
||||||
calibration: CalibrationConfig = field(default_factory=lambda: create_default_calibration(0))
|
calibration: CalibrationConfig = field(
|
||||||
|
default_factory=lambda: CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||||
|
)
|
||||||
|
led_count: int = 0 # explicit LED count; 0 = auto (derived from calibration)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
d = super().to_dict()
|
d = super().to_dict()
|
||||||
@@ -127,4 +131,5 @@ class PictureColorStripSource(ColorStripSource):
|
|||||||
d["smoothing"] = self.smoothing
|
d["smoothing"] = self.smoothing
|
||||||
d["interpolation_mode"] = self.interpolation_mode
|
d["interpolation_mode"] = self.interpolation_mode
|
||||||
d["calibration"] = calibration_to_dict(self.calibration)
|
d["calibration"] = calibration_to_dict(self.calibration)
|
||||||
|
d["led_count"] = self.led_count
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from wled_controller.core.capture.calibration import calibration_to_dict
|
from wled_controller.core.capture.calibration import CalibrationConfig, calibration_to_dict
|
||||||
from wled_controller.storage.color_strip_source import (
|
from wled_controller.storage.color_strip_source import (
|
||||||
ColorStripSource,
|
ColorStripSource,
|
||||||
PictureColorStripSource,
|
PictureColorStripSource,
|
||||||
@@ -98,6 +98,7 @@ class ColorStripStore:
|
|||||||
smoothing: float = 0.3,
|
smoothing: float = 0.3,
|
||||||
interpolation_mode: str = "average",
|
interpolation_mode: str = "average",
|
||||||
calibration=None,
|
calibration=None,
|
||||||
|
led_count: int = 0,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
) -> ColorStripSource:
|
) -> ColorStripSource:
|
||||||
"""Create a new color strip source.
|
"""Create a new color strip source.
|
||||||
@@ -105,8 +106,6 @@ class ColorStripStore:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If validation fails
|
ValueError: If validation fails
|
||||||
"""
|
"""
|
||||||
from wled_controller.core.capture.calibration import create_default_calibration
|
|
||||||
|
|
||||||
if not name or not name.strip():
|
if not name or not name.strip():
|
||||||
raise ValueError("Name is required")
|
raise ValueError("Name is required")
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ class ColorStripStore:
|
|||||||
raise ValueError(f"Color strip source with name '{name}' already exists")
|
raise ValueError(f"Color strip source with name '{name}' already exists")
|
||||||
|
|
||||||
if calibration is None:
|
if calibration is None:
|
||||||
calibration = create_default_calibration(0)
|
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||||
|
|
||||||
source_id = f"css_{uuid.uuid4().hex[:8]}"
|
source_id = f"css_{uuid.uuid4().hex[:8]}"
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
@@ -135,6 +134,7 @@ class ColorStripStore:
|
|||||||
smoothing=smoothing,
|
smoothing=smoothing,
|
||||||
interpolation_mode=interpolation_mode,
|
interpolation_mode=interpolation_mode,
|
||||||
calibration=calibration,
|
calibration=calibration,
|
||||||
|
led_count=led_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._sources[source_id] = source
|
self._sources[source_id] = source
|
||||||
@@ -155,6 +155,7 @@ class ColorStripStore:
|
|||||||
smoothing: Optional[float] = None,
|
smoothing: Optional[float] = None,
|
||||||
interpolation_mode: Optional[str] = None,
|
interpolation_mode: Optional[str] = None,
|
||||||
calibration=None,
|
calibration=None,
|
||||||
|
led_count: Optional[int] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
) -> ColorStripSource:
|
) -> ColorStripSource:
|
||||||
"""Update an existing color strip source.
|
"""Update an existing color strip source.
|
||||||
@@ -193,6 +194,8 @@ class ColorStripStore:
|
|||||||
source.interpolation_mode = interpolation_mode
|
source.interpolation_mode = interpolation_mode
|
||||||
if calibration is not None:
|
if calibration is not None:
|
||||||
source.calibration = calibration
|
source.calibration = calibration
|
||||||
|
if led_count is not None:
|
||||||
|
source.led_count = led_count
|
||||||
|
|
||||||
source.updated_at = datetime.utcnow()
|
source.updated_at = datetime.utcnow()
|
||||||
self._save()
|
self._save()
|
||||||
|
|||||||
@@ -18,6 +18,16 @@
|
|||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.test_device.hint">Select a device to send test pixels to when clicking edge toggles</small>
|
<small class="input-hint" style="display:none" data-i18n="color_strip.test_device.hint">Select a device to send test pixels to when clicking edge toggles</small>
|
||||||
<select id="calibration-test-device"></select>
|
<select id="calibration-test-device"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- LED count input (CSS calibration mode only) -->
|
||||||
|
<div id="cal-css-led-count-group" class="form-group" style="display:none; margin-bottom: 12px; padding: 0 4px;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="cal-css-led-count" data-i18n="color_strip.led_count">LED Count:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
|
||||||
|
<input type="number" id="cal-css-led-count" min="0" max="1500" step="1" value="0" oninput="updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
||||||
<div style="margin-bottom: 12px; padding: 0 24px;">
|
<div style="margin-bottom: 12px; padding: 0 24px;">
|
||||||
<div class="calibration-preview">
|
<div class="calibration-preview">
|
||||||
|
|||||||
@@ -63,40 +63,54 @@
|
|||||||
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
|
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<details class="form-collapse">
|
||||||
<div class="label-row">
|
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
|
||||||
<label for="css-editor-brightness">
|
<div class="form-collapse-body">
|
||||||
<span data-i18n="color_strip.brightness">Brightness:</span>
|
<div class="form-group">
|
||||||
<span id="css-editor-brightness-value">1.00</span>
|
<div class="label-row">
|
||||||
</label>
|
<label for="css-editor-brightness">
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
<span data-i18n="color_strip.brightness">Brightness:</span>
|
||||||
|
<span id="css-editor-brightness-value">1.00</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
|
||||||
|
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="css-editor-saturation">
|
||||||
|
<span data-i18n="color_strip.saturation">Saturation:</span>
|
||||||
|
<span id="css-editor-saturation-value">1.00</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
|
||||||
|
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="css-editor-gamma">
|
||||||
|
<span data-i18n="color_strip.gamma">Gamma:</span>
|
||||||
|
<span id="css-editor-gamma-value">1.00</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)</small>
|
||||||
|
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
|
</details>
|
||||||
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="css-editor-saturation">
|
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
|
||||||
<span data-i18n="color_strip.saturation">Saturation:</span>
|
|
||||||
<span id="css-editor-saturation-value">1.00</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
|
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
|
||||||
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
|
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="label-row">
|
|
||||||
<label for="css-editor-gamma">
|
|
||||||
<span data-i18n="color_strip.gamma">Gamma:</span>
|
|
||||||
<span id="css-editor-gamma-value">1.00</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
||||||
</div>
|
|
||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)</small>
|
|
||||||
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="css-editor-error" class="error-message" style="display: none;"></div>
|
<div id="css-editor-error" class="error-message" style="display: none;"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user