Compare commits

...

2 Commits

Author SHA1 Message Date
68ce394ccc Move overlay toggle into calibration visual editor, add tutorial step
Place the overlay button inside the preview screen as a pill toggle,
add it as a tutorial step that auto-skips in device calibration mode.
Tutorial engine now skips hidden/missing targets in both directions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:50:39 +03:00
f2f67493b1 Fix LED overlay tick positions and reverse handling
Use i/(count-1) fraction (matching calibration dialog) so LEDs span
the full edge, and apply seg.reverse flag for correct numbering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:50:33 +03:00
7 changed files with 65 additions and 18 deletions

View File

@@ -142,7 +142,9 @@ class OverlayWindow:
for i in range(count):
display_idx = (led_index + self.calibration.offset) % total_leds
if i == 0 or i == count - 1 or display_idx % tick_interval == 0:
frac = i / count if count > 1 else 0.5
frac = i / (count - 1) if count > 1 else 0.5
if seg.reverse:
frac = 1.0 - frac
x = start_px + frac * (end_px - start_px)
tick_len = 15 if display_idx % tick_interval == 0 else 10
self._canvas.create_line(x, y_axis - tick_len, x, y_axis + tick_len, fill="white", width=2)
@@ -160,7 +162,9 @@ class OverlayWindow:
for i in range(count):
display_idx = (led_index + self.calibration.offset) % total_leds
if i == 0 or i == count - 1 or display_idx % tick_interval == 0:
frac = i / count if count > 1 else 0.5
frac = i / (count - 1) if count > 1 else 0.5
if seg.reverse:
frac = 1.0 - frac
x = start_px + frac * (end_px - start_px)
tick_len = 15 if display_idx % tick_interval == 0 else 10
self._canvas.create_line(x, y_axis - tick_len, x, y_axis + tick_len, fill="white", width=2)
@@ -178,7 +182,9 @@ class OverlayWindow:
for i in range(count):
display_idx = (led_index + self.calibration.offset) % total_leds
if i == 0 or i == count - 1 or display_idx % tick_interval == 0:
frac = i / count if count > 1 else 0.5
frac = i / (count - 1) if count > 1 else 0.5
if seg.reverse:
frac = 1.0 - frac
y = start_px + frac * (end_px - start_px)
tick_len = 15 if display_idx % tick_interval == 0 else 10
self._canvas.create_line(x_axis - tick_len, y, x_axis + tick_len, y, fill="white", width=2)
@@ -196,7 +202,9 @@ class OverlayWindow:
for i in range(count):
display_idx = (led_index + self.calibration.offset) % total_leds
if i == 0 or i == count - 1 or display_idx % tick_interval == 0:
frac = i / count if count > 1 else 0.5
frac = i / (count - 1) if count > 1 else 0.5
if seg.reverse:
frac = 1.0 - frac
y = start_px + frac * (end_px - start_px)
tick_len = 15 if display_idx % tick_interval == 0 else 10
self._canvas.create_line(x_axis - tick_len, y, x_axis + tick_len, y, fill="white", width=2)

View File

@@ -416,6 +416,34 @@
font-size: 14px;
}
/* Overlay toggle inside the preview screen */
.calibration-overlay-toggle {
display: flex;
align-items: center;
gap: 4px;
height: 26px;
padding: 0 10px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
color: white;
font-family: inherit;
font-size: 12px;
box-sizing: border-box;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
user-select: none;
}
.calibration-overlay-toggle:hover {
background: rgba(255, 255, 255, 0.25);
}
.calibration-overlay-toggle.active {
background: rgba(76, 175, 80, 0.35);
border-color: rgba(76, 175, 80, 0.7);
}
.preview-hint {
text-align: center;
font-size: 0.8rem;

View File

@@ -93,13 +93,7 @@ async function _clearCSSTestMode() {
function _setOverlayBtnActive(active) {
const btn = document.getElementById('calibration-overlay-btn');
if (!btn) return;
if (active) {
btn.style.background = 'var(--primary-color)';
btn.style.color = 'white';
} else {
btn.style.background = '';
btn.style.color = '';
}
btn.classList.toggle('active', active);
}
async function _checkOverlayStatus(cssId) {
@@ -166,7 +160,6 @@ export async function showCalibration(deviceId) {
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
document.getElementById('cal-css-led-count-group').style.display = 'none';
document.getElementById('calibration-overlay-btn').style.display = 'none';
document.getElementById('calibration-tutorial-btn').style.marginLeft = '';
document.getElementById('cal-start-position').value = calibration.start_position;
document.getElementById('cal-layout').value = calibration.layout;
@@ -324,7 +317,6 @@ export async function showCSSCalibration(cssId) {
_overlayStartedHere = false;
const overlayBtn = document.getElementById('calibration-overlay-btn');
overlayBtn.style.display = '';
document.getElementById('calibration-tutorial-btn').style.marginLeft = '0';
_setOverlayBtnActive(false);
_checkOverlayStatus(cssId);

View File

@@ -11,6 +11,7 @@ const calibrationTutorialSteps = [
{ selector: '.direction-toggle', textKey: 'calibration.tip.direction', position: 'bottom' },
{ selector: '.edge-span-bar[data-edge="top"]', textKey: 'calibration.tip.span', position: 'bottom' },
{ selector: '.toggle-top', textKey: 'calibration.tip.test', position: 'top' },
{ selector: '#calibration-overlay-btn', textKey: 'calibration.tip.overlay', position: 'bottom' },
{ selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'top' },
{ selector: '.preview-screen-border-width', textKey: 'calibration.tip.border_width', position: 'bottom' },
{ selector: '#cal-offset', textKey: 'calibration.tip.offset', position: 'top' },
@@ -55,7 +56,13 @@ export function startCalibrationTutorial() {
overlayId: 'tutorial-overlay',
mode: 'absolute',
container: container,
resolveTarget: (step) => document.querySelector(step.selector)
resolveTarget: (step) => {
const el = document.querySelector(step.selector);
if (!el) return null;
// Skip elements hidden via display:none (e.g. overlay btn in device mode)
if (el.style.display === 'none' || getComputedStyle(el).display === 'none') return null;
return el;
}
});
}
@@ -102,11 +109,11 @@ export function tutorialNext() {
export function tutorialPrev() {
if (!activeTutorial) return;
if (activeTutorial.step > 0) {
showTutorialStep(activeTutorial.step - 1);
showTutorialStep(activeTutorial.step - 1, -1);
}
}
function showTutorialStep(index) {
function showTutorialStep(index, direction = 1) {
if (!activeTutorial) return;
activeTutorial.step = index;
const step = activeTutorial.steps[index];
@@ -119,7 +126,13 @@ function showTutorialStep(index) {
});
const target = activeTutorial.resolveTarget(step);
if (!target) return;
if (!target) {
// Auto-skip hidden/missing targets in the current direction
const next = index + direction;
if (next >= 0 && next < activeTutorial.steps.length) showTutorialStep(next, direction);
else closeTutorial();
return;
}
target.classList.add('tutorial-target');
if (isFixed) target.style.zIndex = '10001';

View File

@@ -208,11 +208,13 @@
"calibration.tip.offset": "Set LED offset — distance from LED 0 to the start corner",
"calibration.tip.span": "Drag green bars to adjust coverage span",
"calibration.tip.test": "Click an edge to toggle test LEDs",
"calibration.tip.overlay": "Toggle screen overlay to see LED positions and numbering on your monitor",
"calibration.tip.toggle_inputs": "Click total LED count to toggle edge inputs",
"calibration.tip.border_width": "How many pixels from the screen edge to sample for LED colors",
"calibration.tip.skip_leds_start": "Skip LEDs at the start of the strip — skipped LEDs stay off",
"calibration.tip.skip_leds_end": "Skip LEDs at the end of the strip — skipped LEDs stay off",
"calibration.tutorial.start": "Start tutorial",
"calibration.overlay_toggle": "Overlay",
"calibration.start_position": "Starting Position:",
"calibration.position.bottom_left": "Bottom Left",
"calibration.position.bottom_right": "Bottom Right",

View File

@@ -208,11 +208,13 @@
"calibration.tip.offset": "Смещение LED — расстояние от LED 0 до стартового угла",
"calibration.tip.span": "Перетащите зелёные полосы для настройки зоны покрытия",
"calibration.tip.test": "Нажмите на край для теста LED",
"calibration.tip.overlay": "Включите оверлей для отображения позиций и нумерации LED на мониторе",
"calibration.tip.toggle_inputs": "Нажмите на общее количество LED для скрытия боковых полей",
"calibration.tip.border_width": "Сколько пикселей от края экрана использовать для цветов LED",
"calibration.tip.skip_leds_start": "Пропуск LED в начале ленты — пропущенные LED остаются выключенными",
"calibration.tip.skip_leds_end": "Пропуск LED в конце ленты — пропущенные LED остаются выключенными",
"calibration.tutorial.start": "Начать обучение",
"calibration.overlay_toggle": "Оверлей",
"calibration.start_position": "Начальная Позиция:",
"calibration.position.bottom_left": "Нижний Левый",
"calibration.position.bottom_right": "Нижний Правый",

View File

@@ -3,7 +3,6 @@
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2 id="calibration-modal-title" data-i18n="calibration.title">📐 LED Calibration</h2>
<button id="calibration-overlay-btn" class="tutorial-trigger-btn" onclick="toggleCalibrationOverlay()" data-i18n-title="overlay.button.show" title="Show overlay visualization" style="display:none">&#x1F4A1;</button>
<button id="calibration-tutorial-btn" class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button>
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
@@ -42,6 +41,9 @@
<label for="cal-border-width" data-i18n="calibration.border_width">Border (px):</label>
<input type="number" id="cal-border-width" min="1" max="100" value="10">
</div>
<button id="calibration-overlay-btn" class="calibration-overlay-toggle" onclick="toggleCalibrationOverlay()" data-i18n-title="overlay.button.show" title="Show overlay visualization" style="display:none">
&#x1F4A1; <span data-i18n="calibration.overlay_toggle">Overlay</span>
</button>
</div>
<!-- Edge bars with span controls and LED count inputs -->