Prioritize round-number ticks, add calibration tip list with i18n

Tick labels: round-number ticks (300, 900, etc.) now take priority over
edge boundary labels (288, 933). When they overlap, the boundary label
is suppressed but its tick line is preserved.

Calibration tips: convert single paragraph to bulleted list with
individual i18n keys, add tip about toggling edge inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 03:25:34 +03:00
parent cf019318a6
commit fa322ee0ce
5 changed files with 84 additions and 22 deletions

View File

@@ -1297,23 +1297,28 @@ function renderCalibrationCanvas() {
const count = seg.led_count;
if (count === 0) return;
// Mandatory ticks: first and last LED index per edge, plus LED 0 if offset > 0
const labelsToShow = new Set();
labelsToShow.add(0);
if (count > 1) labelsToShow.add(count - 1);
// Edge boundary ticks (first/last LED on edge) and special ticks (LED 0 position)
const edgeBounds = new Set();
edgeBounds.add(0);
if (count > 1) edgeBounds.add(count - 1);
const specialTicks = new Set();
if (offset > 0 && totalLeds > 0) {
const zeroPos = (totalLeds - seg.led_start % totalLeds) % totalLeds;
if (zeroPos < count) labelsToShow.add(zeroPos);
if (zeroPos < count) specialTicks.add(zeroPos);
}
// Add intermediate ticks at "nice" intervals (max 5 labels per edge)
// Round-number ticks get priority; edge boundary labels suppressed if overlapping
const labelsToShow = new Set([...specialTicks]);
const tickLinesOnly = new Set();
if (count > 2) {
const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1);
const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length;
const minSpacing = geo.horizontal ? maxDigits * 7 + 8 : 22;
const maxIntermediate = Math.max(0, 5 - labelsToShow.size);
const allMandatory = new Set([...edgeBounds, ...specialTicks]);
const maxIntermediate = Math.max(0, 5 - allMandatory.size);
const niceSteps = [5, 10, 25, 50, 100, 250, 500];
let step = niceSteps[niceSteps.length - 1];
for (const s of niceSteps) {
@@ -1323,18 +1328,17 @@ function renderCalibrationCanvas() {
}
}
// Pixel position helper (0..edgeLen along the edge)
const tickPx = i => {
const f = i / (count - 1);
return (seg.reverse ? (1 - f) : f) * edgeLen;
};
// Collect pixel positions of mandatory ticks
// Phase 1: place round-number ticks (checked against specials + each other)
const placed = [];
labelsToShow.forEach(i => placed.push(tickPx(i)));
specialTicks.forEach(i => placed.push(tickPx(i)));
// Add ticks at LED indices divisible by step
for (let i = 1; i < count - 1; i++) {
if (specialTicks.has(i)) continue;
const idx = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
if (idx % step === 0) {
const px = tickPx(i);
@@ -1344,9 +1348,23 @@ function renderCalibrationCanvas() {
}
}
}
// Phase 2: edge boundaries — show label unless overlapping a round-number tick
edgeBounds.forEach(bi => {
if (labelsToShow.has(bi) || specialTicks.has(bi)) return;
const px = tickPx(bi);
if (placed.some(p => Math.abs(px - p) < minSpacing)) {
tickLinesOnly.add(bi);
} else {
labelsToShow.add(bi);
placed.push(px);
}
});
} else {
edgeBounds.forEach(i => labelsToShow.add(i));
}
// Tick styling — min/max ticks extend to container border, others short
// Tick styling
const tickLenLong = toggleSize + 3;
const tickLenShort = 4;
ctx.strokeStyle = tickStroke;
@@ -1354,45 +1372,66 @@ function renderCalibrationCanvas() {
ctx.fillStyle = tickFill;
ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';
// Draw labeled ticks
labelsToShow.forEach(i => {
const fraction = count > 1 ? i / (count - 1) : 0.5;
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
const ledIndex = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
const tickLen = (i === 0 || i === count - 1) ? tickLenLong : tickLenShort;
const tickLen = edgeBounds.has(i) ? tickLenLong : tickLenShort;
if (geo.horizontal) {
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
const axisY = axisPos[seg.edge];
const tickDir = seg.edge === 'top' ? 1 : -1; // tick toward container
const tickDir = seg.edge === 'top' ? 1 : -1;
// Tick line
ctx.beginPath();
ctx.moveTo(tx, axisY);
ctx.lineTo(tx, axisY + tickDir * tickLen);
ctx.stroke();
// Label outside
ctx.textAlign = 'center';
ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top';
ctx.fillText(String(ledIndex), tx, axisY - tickDir * 1);
} else {
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
const axisX = axisPos[seg.edge];
const tickDir = seg.edge === 'left' ? 1 : -1; // tick toward container
const tickDir = seg.edge === 'left' ? 1 : -1;
// Tick line
ctx.beginPath();
ctx.moveTo(axisX, ty);
ctx.lineTo(axisX + tickDir * tickLen, ty);
ctx.stroke();
// Label outside
ctx.textBaseline = 'middle';
ctx.textAlign = seg.edge === 'left' ? 'right' : 'left';
ctx.fillText(String(ledIndex), axisX - tickDir * 1, ty);
}
});
// Draw tick lines only (no labels) for suppressed edge boundaries
tickLinesOnly.forEach(i => {
const fraction = count > 1 ? i / (count - 1) : 0.5;
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
if (geo.horizontal) {
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
const axisY = axisPos[seg.edge];
const tickDir = seg.edge === 'top' ? 1 : -1;
ctx.beginPath();
ctx.moveTo(tx, axisY);
ctx.lineTo(tx, axisY + tickDir * tickLenLong);
ctx.stroke();
} else {
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
const axisX = axisPos[seg.edge];
const tickDir = seg.edge === 'left' ? 1 : -1;
ctx.beginPath();
ctx.moveTo(axisX, ty);
ctx.lineTo(axisX + tickDir * tickLenLong, ty);
ctx.stroke();
}
});
// Draw direction chevron at full-edge midpoint (not affected by span)
const s = 7;
let mx, my, angle;

View File

@@ -84,7 +84,13 @@
</div>
<div class="modal-body">
<input type="hidden" id="calibration-device-id">
<p class="section-tip" data-i18n="calibration.preview.click_hint">Click an edge to toggle test LEDs on/off</p>
<ul class="section-tip">
<li data-i18n="calibration.tip.led_count">Enter LED count per edge</li>
<li data-i18n="calibration.tip.start_corner">Click a corner to set the start position</li>
<li data-i18n="calibration.tip.span">Drag green bars to adjust coverage span</li>
<li data-i18n="calibration.tip.test">Click an edge to toggle test LEDs</li>
<li data-i18n="calibration.tip.toggle_inputs">Click total LED count to toggle edge inputs</li>
</ul>
<!-- Interactive Preview with integrated LED inputs and test toggles -->
<div style="margin-bottom: 12px; padding: 0 24px;">
<div class="calibration-preview">

View File

@@ -90,7 +90,11 @@
"settings.saved": "Settings saved successfully",
"settings.failed": "Failed to save settings",
"calibration.title": "LED Calibration",
"calibration.preview.click_hint": "Click an edge to toggle test LEDs on/off",
"calibration.tip.led_count": "Enter LED count per edge",
"calibration.tip.start_corner": "Click a corner to set the start position",
"calibration.tip.span": "Drag green bars to adjust coverage span",
"calibration.tip.test": "Click an edge to toggle test LEDs",
"calibration.tip.toggle_inputs": "Click total LED count to toggle edge inputs",
"calibration.start_position": "Starting Position:",
"calibration.position.bottom_left": "Bottom Left",
"calibration.position.bottom_right": "Bottom Right",

View File

@@ -90,7 +90,11 @@
"settings.saved": "Настройки успешно сохранены",
"settings.failed": "Не удалось сохранить настройки",
"calibration.title": "Калибровка Светодиодов",
"calibration.preview.click_hint": "Нажмите на край чтобы включить/выключить тест светодиодов",
"calibration.tip.led_count": "Укажите количество LED на каждой стороне",
"calibration.tip.start_corner": "Нажмите на угол для выбора стартовой позиции",
"calibration.tip.span": "Перетащите зелёные полосы для настройки зоны покрытия",
"calibration.tip.test": "Нажмите на край для теста LED",
"calibration.tip.toggle_inputs": "Нажмите на общее количество LED для скрытия боковых полей",
"calibration.start_position": "Начальная Позиция:",
"calibration.position.bottom_left": "Нижний Левый",
"calibration.position.bottom_right": "Нижний Правый",

View File

@@ -629,6 +629,15 @@ section {
text-decoration: underline;
}
ul.section-tip {
list-style: disc;
padding-left: 28px;
}
ul.section-tip li {
margin: 2px 0;
}
.form-group {
margin-bottom: 15px;
}