webui: add calibration editor to the V-Scale toolbar

Adds the Cal row (Scale / Offset / Unit / Reset) to #vscale-menu, its
CSS, and the refreshVScaleMenu() / commitCal() logic that wires it to
calTable and the hub via setCalibrationWS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-17 00:48:39 +02:00
co-authored by Claude Sonnet 4.6
parent 0e9ec61226
commit 4e3a90b2c6
3 changed files with 85 additions and 0 deletions
+68
View File
@@ -3514,6 +3514,32 @@ function escHtml(s) {
════════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════════ */
let _vsMenuKey = null, _vsMenuPlotId = null; let _vsMenuKey = null, _vsMenuPlotId = null;
// Re-read the calibration fields from calTable for the currently open toolbar.
// Safe to call when the toolbar is closed.
function refreshVScaleMenu() {
if (!_vsMenuKey) return;
const cal = calForKey(_vsMenuKey);
const base = baseSigForKey(_vsMenuKey);
const meta = findSignalMeta(_vsMenuKey);
const n = meta ? numElements(meta) : 1;
const lbl = document.getElementById('vscale-cal-lbl');
lbl.textContent = n > 1 ? 'Cal (' + base + ', ' + n + ' elem)' : 'Cal (' + base + ')';
const scaleEl = document.getElementById('vscale-cal-scale');
const offsetEl = document.getElementById('vscale-cal-offset');
const unitEl = document.getElementById('vscale-cal-unit');
// Skip the field the user is currently typing in, so a hub broadcast does not
// yank the caret out from under them.
const focused = document.activeElement;
if (focused !== scaleEl) scaleEl.value = cal.scale;
if (focused !== offsetEl) offsetEl.value = cal.offset;
if (focused !== unitEl) unitEl.value = cal.unit;
[scaleEl, offsetEl, unitEl].forEach(el => el.classList.remove('cal-invalid'));
const srcLabel = srcLabelForKey(_vsMenuKey);
const usable = srcLabel !== '' && base !== '';
[scaleEl, offsetEl, unitEl, document.getElementById('btn-cal-reset')]
.forEach(el => { el.disabled = !usable; });
}
function showVScaleMenu(key, plotId) { function showVScaleMenu(key, plotId) {
hideSignalMenu(); hideSignalMenu();
// If the toolbar was open for a different plot, hide that bar first. // If the toolbar was open for a different plot, hide that bar first.
@@ -3562,6 +3588,8 @@ function showVScaleMenu(key, plotId) {
btn.classList.toggle('active', btn.dataset.type === (vs.digitalInMixed ? 'digital' : 'analog'))); btn.classList.toggle('active', btn.dataset.type === (vs.digitalInMixed ? 'digital' : 'analog')));
} }
refreshVScaleMenu();
// Move the toolbar div into this plot's vscale bar. // Move the toolbar div into this plot's vscale bar.
const bar = document.getElementById('vstb-' + plotId); const bar = document.getElementById('vstb-' + plotId);
if (bar) { if (bar) {
@@ -3686,6 +3714,46 @@ function initVScaleMenu() {
if (p) { createUPlot(p); p.needsRedraw = true; } if (p) { createUPlot(p); p.needsRedraw = true; }
}); });
}); });
// ── Calibration ───────────────────────────────────────────────────────
// Commit the three fields as one entry. Validation mirrors the hub exactly
// (Calib.normaliseCal); an invalid value marks the field and is not sent, so
// the last accepted value stays in force.
function commitCal() {
if (!_vsMenuKey) return;
const scaleEl = document.getElementById('vscale-cal-scale');
const offsetEl = document.getElementById('vscale-cal-offset');
const unitEl = document.getElementById('vscale-cal-unit');
const source = srcLabelForKey(_vsMenuKey);
const signal = baseSigForKey(_vsMenuKey);
const entry = Calib.normaliseCal({
source, signal,
scale: parseFloat(scaleEl.value),
offset: parseFloat(offsetEl.value),
unit: unitEl.value,
});
const scaleBad = entry === null && !(isFinite(parseFloat(scaleEl.value)) && parseFloat(scaleEl.value) !== 0);
scaleEl.classList.toggle('cal-invalid', scaleBad);
offsetEl.classList.toggle('cal-invalid', entry === null && !isFinite(parseFloat(offsetEl.value)));
if (entry === null) return;
calTable.set(entry);
setCalibrationWS(entry.source, entry.signal, entry.scale, entry.offset, entry.unit);
applyCalibrationChanged();
}
document.getElementById('vscale-cal-scale').addEventListener('change', commitCal);
document.getElementById('vscale-cal-offset').addEventListener('change', commitCal);
document.getElementById('vscale-cal-unit').addEventListener('change', commitCal);
document.getElementById('btn-cal-reset').addEventListener('click', () => {
if (!_vsMenuKey) return;
const source = srcLabelForKey(_vsMenuKey);
const signal = baseSigForKey(_vsMenuKey);
if (!source || !signal) return;
calTable.set({ source, signal, scale: 1, offset: 0, unit: '' });
setCalibrationWS(source, signal, 1, 0, '');
applyCalibrationChanged();
refreshVScaleMenu();
});
document.getElementById('btn-vscale-close').addEventListener('click', hideVScaleMenu); document.getElementById('btn-vscale-close').addEventListener('click', hideVScaleMenu);
} }
+12
View File
@@ -211,6 +211,18 @@
<button class="ctx-btn" data-type="digital">Digital</button> <button class="ctx-btn" data-type="digital">Digital</button>
</div> </div>
</div> </div>
<div class="vstb-sep"></div>
<div id="vscale-cal-row" style="display:flex;align-items:center;gap:4px">
<label class="vstb-lbl" id="vscale-cal-lbl"
title="Data calibration: value = raw × Scale + Offset. Applies to the plot, cursors, hover readout, CSV export and trigger threshold.">Cal</label>
<label class="vstb-lbl">Scale</label>
<input type="number" id="vscale-cal-scale" class="ctx-num ctx-num-sm" step="any" value="1">
<label class="vstb-lbl">Offset</label>
<input type="number" id="vscale-cal-offset" class="ctx-num ctx-num-sm" step="any" value="0">
<label class="vstb-lbl">Unit</label>
<input type="text" id="vscale-cal-unit" class="ctx-num ctx-num-xs" maxlength="16" placeholder="—">
<button class="ctx-btn" id="btn-cal-reset" title="Clear this signal's calibration">Reset</button>
</div>
<button id="btn-vscale-close" class="vstb-close" title="Close"></button> <button id="btn-vscale-close" class="vstb-close" title="Close"></button>
</div> </div>
</div> </div>
+5
View File
@@ -386,6 +386,11 @@ input[type=range].trig-range::-webkit-slider-thumb {
} }
.vstb-close:hover { color:var(--red); } .vstb-close:hover { color:var(--red); }
.plot-vscale-bar { display:none; } .plot-vscale-bar { display:none; }
.vstb-sep { width:1px; height:16px; background:var(--surface1); flex-shrink:0; }
.ctx-num-sm { width:70px; }
.ctx-num-xs { width:46px; }
#vscale-cal-lbl { color:var(--mauve); font-weight:600; }
.cal-invalid { border-color:var(--red) !important; }
/* ── Per-plot cursor value readout (in plot card header) ────────── */ /* ── Per-plot cursor value readout (in plot card header) ────────── */
.plot-cursor-ro { .plot-cursor-ro {