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; const count = seg.led_count;
if (count === 0) return; if (count === 0) return;
// Mandatory ticks: first and last LED index per edge, plus LED 0 if offset > 0 // Edge boundary ticks (first/last LED on edge) and special ticks (LED 0 position)
const labelsToShow = new Set(); const edgeBounds = new Set();
labelsToShow.add(0); edgeBounds.add(0);
if (count > 1) labelsToShow.add(count - 1); if (count > 1) edgeBounds.add(count - 1);
const specialTicks = new Set();
if (offset > 0 && totalLeds > 0) { if (offset > 0 && totalLeds > 0) {
const zeroPos = (totalLeds - seg.led_start % totalLeds) % totalLeds; 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) { if (count > 2) {
const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1); const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1);
const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length; const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length;
const minSpacing = geo.horizontal ? maxDigits * 7 + 8 : 22; 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]; const niceSteps = [5, 10, 25, 50, 100, 250, 500];
let step = niceSteps[niceSteps.length - 1]; let step = niceSteps[niceSteps.length - 1];
for (const s of niceSteps) { for (const s of niceSteps) {
@@ -1323,18 +1328,17 @@ function renderCalibrationCanvas() {
} }
} }
// Pixel position helper (0..edgeLen along the edge)
const tickPx = i => { const tickPx = i => {
const f = i / (count - 1); const f = i / (count - 1);
return (seg.reverse ? (1 - f) : f) * edgeLen; 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 = []; 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++) { 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; const idx = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
if (idx % step === 0) { if (idx % step === 0) {
const px = tickPx(i); 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 tickLenLong = toggleSize + 3;
const tickLenShort = 4; const tickLenShort = 4;
ctx.strokeStyle = tickStroke; ctx.strokeStyle = tickStroke;
@@ -1354,45 +1372,66 @@ function renderCalibrationCanvas() {
ctx.fillStyle = tickFill; ctx.fillStyle = tickFill;
ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif'; ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';
// Draw labeled ticks
labelsToShow.forEach(i => { labelsToShow.forEach(i => {
const fraction = count > 1 ? i / (count - 1) : 0.5; const fraction = count > 1 ? i / (count - 1) : 0.5;
const displayFraction = seg.reverse ? (1 - fraction) : fraction; const displayFraction = seg.reverse ? (1 - fraction) : fraction;
const ledIndex = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i; 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) { if (geo.horizontal) {
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1); const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
const axisY = axisPos[seg.edge]; 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.beginPath();
ctx.moveTo(tx, axisY); ctx.moveTo(tx, axisY);
ctx.lineTo(tx, axisY + tickDir * tickLen); ctx.lineTo(tx, axisY + tickDir * tickLen);
ctx.stroke(); ctx.stroke();
// Label outside
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top'; ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top';
ctx.fillText(String(ledIndex), tx, axisY - tickDir * 1); ctx.fillText(String(ledIndex), tx, axisY - tickDir * 1);
} else { } else {
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1); const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
const axisX = axisPos[seg.edge]; 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.beginPath();
ctx.moveTo(axisX, ty); ctx.moveTo(axisX, ty);
ctx.lineTo(axisX + tickDir * tickLen, ty); ctx.lineTo(axisX + tickDir * tickLen, ty);
ctx.stroke(); ctx.stroke();
// Label outside
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.textAlign = seg.edge === 'left' ? 'right' : 'left'; ctx.textAlign = seg.edge === 'left' ? 'right' : 'left';
ctx.fillText(String(ledIndex), axisX - tickDir * 1, ty); 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) // Draw direction chevron at full-edge midpoint (not affected by span)
const s = 7; const s = 7;
let mx, my, angle; let mx, my, angle;

View File

@@ -84,7 +84,13 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="hidden" id="calibration-device-id"> <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 --> <!-- 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">

View File

@@ -90,7 +90,11 @@
"settings.saved": "Settings saved successfully", "settings.saved": "Settings saved successfully",
"settings.failed": "Failed to save settings", "settings.failed": "Failed to save settings",
"calibration.title": "LED Calibration", "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.start_position": "Starting Position:",
"calibration.position.bottom_left": "Bottom Left", "calibration.position.bottom_left": "Bottom Left",
"calibration.position.bottom_right": "Bottom Right", "calibration.position.bottom_right": "Bottom Right",

View File

@@ -90,7 +90,11 @@
"settings.saved": "Настройки успешно сохранены", "settings.saved": "Настройки успешно сохранены",
"settings.failed": "Не удалось сохранить настройки", "settings.failed": "Не удалось сохранить настройки",
"calibration.title": "Калибровка Светодиодов", "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.start_position": "Начальная Позиция:",
"calibration.position.bottom_left": "Нижний Левый", "calibration.position.bottom_left": "Нижний Левый",
"calibration.position.bottom_right": "Нижний Правый", "calibration.position.bottom_right": "Нижний Правый",

View File

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