Add LED axis ticks and calibration labels to color strip test preview

- Add horizontal axis with tick marks and LED index labels below strip
  and composite preview canvases (first/last labels edge-aligned)
- Show actual/calibration LED count label on picture-based composite
  layers (e.g. "25/934")
- Display warning icon in orange when LED counts don't match
- Send is_picture and calibration_led_count in composite layer_infos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 22:47:22 +03:00
parent d1c8324c0f
commit 7e78323c9c
4 changed files with 171 additions and 182 deletions

View File

@@ -132,6 +132,12 @@
image-rendering: pixelated;
}
.css-test-strip-axis {
display: block;
width: 100%;
height: 20px;
}
.css-test-fire-btn {
position: absolute;
right: 6px;
@@ -306,6 +312,27 @@
}
.css-test-layer-brightness svg { width: 12px; height: 12px; }
.css-test-layer-cal {
position: absolute;
right: 6px;
bottom: 4px;
font-size: 0.6rem;
font-family: var(--font-mono, monospace);
color: #fff;
text-shadow: 0 0 3px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.6);
pointer-events: none;
white-space: nowrap;
opacity: 0.7;
display: flex;
align-items: center;
gap: 2px;
}
.css-test-layer-cal svg { width: 12px; height: 12px; }
.css-test-layer-cal-warn {
color: var(--warning-color, #ff9800);
opacity: 1;
}
/* LED count control */
.css-test-led-control {
display: flex;

View File

@@ -13,7 +13,7 @@ import {
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_EYE,
ICON_SUN_DIM,
ICON_SUN_DIM, ICON_WARNING,
} from '../core/icons.js';
import * as P from '../core/icon-paths.js';
import { wrapCard } from '../core/card-colors.js';
@@ -2024,6 +2024,12 @@ function _cssTestConnect(sourceId, ledCount, fps) {
// Build composite layer canvases
if (_cssTestIsComposite) {
_cssTestBuildLayers(_cssTestMeta.layers, _cssTestMeta.source_type, _cssTestMeta.layer_infos);
requestAnimationFrame(() => _cssTestRenderStripAxis('css-test-layers-axis', _cssTestMeta.led_count));
}
// Render strip axis for non-picture, non-composite views
if (!isPicture && !_cssTestIsComposite) {
requestAnimationFrame(() => _cssTestRenderStripAxis('css-test-strip-axis', _cssTestMeta.led_count));
}
} else {
const raw = new Uint8Array(event.data);
@@ -2110,9 +2116,15 @@ function _cssTestBuildLayers(layerNames, sourceType, layerInfos) {
const briLabel = hasBri
? `<span class="css-test-layer-brightness" data-layer-idx="${i}" style="display:none"></span>`
: '';
let calLabel = '';
if (info && info.is_picture && info.calibration_led_count) {
const mismatch = _cssTestMeta.led_count !== info.calibration_led_count;
calLabel = `<span class="css-test-layer-cal${mismatch ? ' css-test-layer-cal-warn' : ''}" data-layer-idx="${i}">${mismatch ? ICON_WARNING + ' ' : ''}${_cssTestMeta.led_count}/${info.calibration_led_count}</span>`;
}
html += `<div class="css-test-layer css-test-strip-wrap">` +
`<canvas class="css-test-layer-canvas" data-layer-idx="${i}"></canvas>` +
`<span class="css-test-layer-label">${escapeHtml(layerNames[i])}</span>` +
calLabel +
briLabel +
fireBtn +
`</div>`;
@@ -2376,6 +2388,73 @@ function _cssTestRenderTicks(edges) {
}
}
function _cssTestRenderStripAxis(canvasId, ledCount) {
const canvas = document.getElementById(canvasId);
if (!canvas || ledCount <= 0) return;
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, h);
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
const tickStroke = isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.35)';
const tickFill = isDark ? 'rgba(255, 255, 255, 0.75)' : 'rgba(0, 0, 0, 0.65)';
ctx.strokeStyle = tickStroke;
ctx.fillStyle = tickFill;
ctx.lineWidth = 1;
ctx.font = '10px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.textBaseline = 'top';
const tickLen = 5;
// Determine which ticks to label
const labelsToShow = new Set([0]);
if (ledCount > 1) labelsToShow.add(ledCount - 1);
if (ledCount > 2) {
const maxDigits = String(ledCount - 1).length;
const minSpacing = maxDigits * 7 + 8;
const niceSteps = [5, 10, 25, 50, 100, 250, 500];
let step = niceSteps[niceSteps.length - 1];
for (const s of niceSteps) {
if (Math.floor(ledCount / s) <= Math.floor(w / minSpacing)) { step = s; break; }
}
const tickPx = i => (i / (ledCount - 1)) * w;
const placed = [];
labelsToShow.forEach(i => placed.push(tickPx(i)));
for (let i = 1; i < ledCount - 1; i++) {
if (i % step === 0) {
const px = tickPx(i);
if (!placed.some(p => Math.abs(px - p) < minSpacing)) {
labelsToShow.add(i);
placed.push(px);
}
}
}
}
labelsToShow.forEach(idx => {
const fraction = ledCount > 1 ? idx / (ledCount - 1) : 0.5;
const tx = fraction * w;
ctx.beginPath();
ctx.moveTo(tx, 0);
ctx.lineTo(tx, tickLen);
ctx.stroke();
// Align first tick left, last tick right, others center
if (idx === 0) ctx.textAlign = 'left';
else if (idx === ledCount - 1) ctx.textAlign = 'right';
else ctx.textAlign = 'center';
ctx.fillText(String(idx), tx, tickLen + 1);
});
}
export function fireCssTestNotification() {
for (const id of _cssTestNotificationIds) {
testNotification(id);