Implemented and fixed many issues
This commit is contained in:
+794
-262
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
// Min/max (peak-envelope) decimation — O(n). Runs off-main-thread to avoid
|
||||
// blocking the render loop.
|
||||
//
|
||||
// The range is split into threshold/2 equal buckets and each contributes its
|
||||
// smallest and largest sample, in the order the two occurred — the way an
|
||||
// oscilloscope draws a trace it cannot show pixel-for-pixel.
|
||||
//
|
||||
// This replaced LTTB, which picks the sample forming the largest triangle with
|
||||
// its neighbours: a plausible-looking shape, but it silently drops a one-sample
|
||||
// spike whenever a smoother neighbour scores higher — exactly the sample worth
|
||||
// looking at. The envelope cannot drop it, because a spike is by definition its
|
||||
// bucket's min or max. Every output point is a real sample at its real
|
||||
// timestamp; nothing is interpolated or averaged.
|
||||
//
|
||||
// Kept identical to minMaxDecimate() in Common/Client/go/wshub/hub.go and to
|
||||
// decimate() in app.js, so a trace looks the same whichever thinned it.
|
||||
function decimate(t, v, threshold) {
|
||||
const len = t.length;
|
||||
if (len <= threshold || threshold < 4) {
|
||||
// Copy to new arrays so we can transfer them back without detaching the input.
|
||||
return { t: new Float64Array(t), v: new Float64Array(v) };
|
||||
}
|
||||
const buckets = threshold >> 1;
|
||||
const outT = new Float64Array(threshold);
|
||||
const outV = new Float64Array(threshold);
|
||||
let n = 0;
|
||||
for (let b = 0; b < buckets; b++) {
|
||||
const lo = Math.floor(b * len / buckets);
|
||||
const hi = (b === buckets - 1) ? len : Math.floor((b + 1) * len / buckets);
|
||||
if (lo >= hi) continue;
|
||||
let iMin = lo, iMax = lo;
|
||||
for (let j = lo + 1; j < hi; j++) {
|
||||
if (v[j] < v[iMin]) iMin = j;
|
||||
if (v[j] > v[iMax]) iMax = j;
|
||||
}
|
||||
// Emit in time order so the result plots as one ascending trace.
|
||||
if (iMin > iMax) { const s = iMin; iMin = iMax; iMax = s; }
|
||||
outT[n] = t[iMin]; outV[n] = v[iMin]; n++;
|
||||
// A bucket whose samples are all equal has one extreme, not two.
|
||||
if (iMax !== iMin) { outT[n] = t[iMax]; outV[n] = v[iMax]; n++; }
|
||||
}
|
||||
// slice() so the transferred buffers are exactly the used length.
|
||||
return { t: outT.slice(0, n), v: outV.slice(0, n) };
|
||||
}
|
||||
|
||||
self.onmessage = function({ data: { id, t, v, threshold } }) {
|
||||
const result = decimate(t, v, threshold);
|
||||
// Transfer the output buffers back to the main thread zero-copy.
|
||||
self.postMessage({ id, t: result.t, v: result.v }, [result.t.buffer, result.v.buffer]);
|
||||
};
|
||||
@@ -30,11 +30,14 @@
|
||||
</div>
|
||||
<span class="ctrl-label" id="lbl-window">Window:</span>
|
||||
<select id="window-select" class="ctrl-select">
|
||||
<option value="1">1 s</option><option value="5" selected>5 s</option>
|
||||
<option value="10">10 s</option><option value="30">30 s</option>
|
||||
<option value="60">60 s</option>
|
||||
<option value="1">1 s</option><option value="2">2 s</option>
|
||||
<option value="5" selected>5 s</option><option value="10">10 s</option>
|
||||
<option value="15">15 s</option><option value="30">30 s</option>
|
||||
<option value="60">60 s</option><option value="120">2 min</option>
|
||||
<option value="300">5 min</option><option value="600">10 min</option>
|
||||
</select>
|
||||
<button id="btn-cursor" class="ctrl-btn">Cursors</button>
|
||||
<button id="btn-cursor-reset" class="ctrl-btn" style="display:none" title="Bring cursors A/B back into the visible window">↔ Reset</button>
|
||||
<button id="btn-ruler" class="ctrl-btn" title="Horizontal value rulers">Rulers</button>
|
||||
<button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button>
|
||||
<button id="btn-zoom-fit" class="ctrl-btn">Fit</button>
|
||||
@@ -43,7 +46,8 @@
|
||||
<button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button>
|
||||
<button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button>
|
||||
<label class="ctrl-check" title="Snap jittery inter-frame timestamps to ideal spacing (eliminates overlaps/gaps from software-dispatch jitter)">
|
||||
<input type="checkbox" id="cb-monotonic"> Sync TS
|
||||
<input type="checkbox" id="cb-monotonic">
|
||||
Sync TS
|
||||
</label>
|
||||
</div>
|
||||
<!-- ── Trigger bar ───────────────────────────────────────────── -->
|
||||
@@ -70,10 +74,19 @@
|
||||
<div class="trig-group">
|
||||
<span class="trig-label">Window</span>
|
||||
<select id="trig-window" class="trig-select">
|
||||
<option value="0.0001">100 μs</option><option value="0.001">1 ms</option>
|
||||
<option value="0.01">10 ms</option><option value="0.1">100 ms</option>
|
||||
<option value="0.5">500 ms</option><option value="1" selected>1 s</option>
|
||||
<option value="0.0001">100 μs</option><option value="0.0002">200 μs</option>
|
||||
<option value="0.0005">500 μs</option><option value="0.001">1 ms</option>
|
||||
<option value="0.002">2 ms</option><option value="0.005">5 ms</option>
|
||||
<option value="0.01">10 ms</option><option value="0.02">20 ms</option>
|
||||
<option value="0.05">50 ms</option><option value="0.1">100 ms</option>
|
||||
<option value="0.2">200 ms</option><option value="0.5">500 ms</option>
|
||||
<option value="1" selected>1 s</option><option value="2">2 s</option>
|
||||
<option value="5">5 s</option><option value="10">10 s</option>
|
||||
<option value="20">20 s</option><option value="30">30 s</option>
|
||||
<option value="60">60 s</option>
|
||||
<option value="120">2 m</option>
|
||||
<option value="300">5 m</option>
|
||||
<option value="600">10 m</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="trig-sep"></div>
|
||||
@@ -83,6 +96,11 @@
|
||||
<span class="trig-range-val" id="trig-pre-val">20%</span>
|
||||
</div>
|
||||
<div class="trig-sep"></div>
|
||||
<div class="trig-group">
|
||||
<span class="trig-label" title="Re-arm delay after a capture — prevents double triggering">Holdoff</span>
|
||||
<input id="trig-holdoff" class="trig-input" type="number" min="0" max="60" step="0.01" value="0.2">
|
||||
<span class="trig-label">s</span>
|
||||
</div>
|
||||
<div class="trig-group">
|
||||
<span class="trig-label">Mode</span>
|
||||
<select id="trig-mode" class="trig-select">
|
||||
@@ -124,10 +142,30 @@
|
||||
<span id="status-text">Disconnected</span>
|
||||
<span id="sb-tsage"></span>
|
||||
<button id="btn-stats" class="ctrl-btn" style="height:16px;padding:0 7px;font-size:10px;line-height:1">📊 Stats</button>
|
||||
<span id="history-badge" style="display:none;font-size:10px;color:#f9e2af;margin-left:8px"></span>
|
||||
<button id="history-badge" style="display:none" title="Disk history — click to set the per-signal budget"></button>
|
||||
</div>
|
||||
<span id="build-version"></span>
|
||||
</div>
|
||||
<!-- ── History budget popup ──────────────────────────────────── -->
|
||||
<div id="history-panel" style="display:none">
|
||||
<div class="ctx-menu-header">Disk history budget</div>
|
||||
<div class="ctx-row">
|
||||
<label>Budget</label>
|
||||
<input type="number" id="hist-budget" class="ctx-num" min="0.001" step="1">
|
||||
<span class="ctx-range-val">MPts/signal</span>
|
||||
</div>
|
||||
<div class="hist-note">
|
||||
The budget buys resolution, not duration: a signal too fast to store
|
||||
sample-for-sample is archived as a min/max envelope wide enough to fit,
|
||||
so the configured window is always covered.
|
||||
</div>
|
||||
<div id="hist-signal-res"></div>
|
||||
<div class="hist-note hist-warn">Applying re-creates the history files — archived data is lost.</div>
|
||||
<div class="ctx-row" style="margin:0;justify-content:flex-end">
|
||||
<button class="ctx-btn" id="btn-hist-cancel">Cancel</button>
|
||||
<button class="ctx-btn" id="btn-hist-apply">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="layout-menu"></div>
|
||||
<!-- ── Signal style context menu ─────────────────────────────── -->
|
||||
<div id="sig-ctx-menu" style="display:none">
|
||||
@@ -172,7 +210,8 @@
|
||||
</div>
|
||||
<!-- ── Array index picker (trigger signal) ──────────────────────── -->
|
||||
<div id="array-idx-picker" style="display:none">
|
||||
<div class="ctx-menu-header">Element index: <span id="aip-sig" class="ctx-menu-key"></span></div>
|
||||
<div class="ctx-menu-header">Element index:
|
||||
<span id="aip-sig" class="ctx-menu-key"></span></div>
|
||||
<div class="ctx-row">
|
||||
<label>Index</label>
|
||||
<input type="number" id="aip-idx" class="ctx-num" min="0" step="1" value="0">
|
||||
@@ -186,7 +225,8 @@
|
||||
<!-- ── VScale toolbar (moved into plot card when active) ─────────── -->
|
||||
<div id="vscale-menu" style="display:none">
|
||||
<div class="vstb-header">
|
||||
<span class="vstb-label">V-Scale: <span id="vscale-menu-key" class="ctx-menu-key"></span></span>
|
||||
<span class="vstb-label"><span id="vscale-menu-title">V-Scale</span>:
|
||||
<span id="vscale-menu-key" class="ctx-menu-key"></span></span>
|
||||
<div class="ctx-btns" id="vscale-mode-btns">
|
||||
<button class="ctx-btn active" data-mode="auto">Auto</button>
|
||||
<button class="ctx-btn" data-mode="range">Range</button>
|
||||
@@ -200,10 +240,6 @@
|
||||
<label class="vstb-lbl" title="Raw value at screen centre — unbounded, may lie outside the plotted range">Offset</label>
|
||||
<input type="number" id="vscale-offset" class="ctx-num" step="any" value="0">
|
||||
</div>
|
||||
<div id="vscale-pos-row" style="display:none;align-items:center;gap:4px">
|
||||
<label class="vstb-lbl">Pos</label>
|
||||
<input type="number" id="vscale-pos" class="ctx-num" step="0.1" value="0">
|
||||
</div>
|
||||
<div id="vscale-type-row" style="display:none;align-items:center;gap:4px">
|
||||
<label class="vstb-lbl">Type</label>
|
||||
<div class="ctx-btns" id="vscale-type-btns">
|
||||
@@ -213,8 +249,7 @@
|
||||
</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" 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>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
'use strict';
|
||||
// LTTB (Largest Triangle Three Buckets) decimation — O(n).
|
||||
// Runs off-main-thread to avoid blocking the render loop.
|
||||
function lttb(t, v, threshold) {
|
||||
const len = t.length;
|
||||
if (len <= threshold) {
|
||||
// Copy to new arrays so we can transfer them back without detaching the input.
|
||||
return { t: new Float64Array(t), v: new Float64Array(v) };
|
||||
}
|
||||
const outT = new Float64Array(threshold);
|
||||
const outV = new Float64Array(threshold);
|
||||
outT[0] = t[0]; outV[0] = v[0];
|
||||
outT[threshold - 1] = t[len - 1]; outV[threshold - 1] = v[len - 1];
|
||||
const every = (len - 2) / (threshold - 2);
|
||||
let a = 0;
|
||||
for (let i = 0; i < threshold - 2; i++) {
|
||||
const avgS = Math.floor((i + 1) * every) + 1;
|
||||
const avgE = Math.min(Math.floor((i + 2) * every) + 1, len);
|
||||
let avgT = 0, avgV = 0, n = 0;
|
||||
for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; }
|
||||
if (n) { avgT /= n; avgV /= n; }
|
||||
const rS = Math.floor(i * every) + 1;
|
||||
const rE = Math.min(Math.floor((i + 1) * every) + 1, len);
|
||||
let maxA = -1, next = rS;
|
||||
const aT = t[a], aV = v[a];
|
||||
for (let j = rS; j < rE; j++) {
|
||||
const area = Math.abs((aT - avgT) * (v[j] - aV) - (aT - t[j]) * (avgV - aV));
|
||||
if (area > maxA) { maxA = area; next = j; }
|
||||
}
|
||||
outT[i + 1] = t[next]; outV[i + 1] = v[next]; a = next;
|
||||
}
|
||||
return { t: outT, v: outV };
|
||||
}
|
||||
|
||||
self.onmessage = function({ data: { id, t, v, threshold } }) {
|
||||
const result = lttb(t, v, threshold);
|
||||
// Transfer the output buffers back to the main thread zero-copy.
|
||||
self.postMessage({ id, t: result.t, v: result.v }, [result.t.buffer, result.v.buffer]);
|
||||
};
|
||||
@@ -141,10 +141,17 @@ input[type=range].trig-range::-webkit-slider-thumb {
|
||||
#trig-status-badge.armed { background:rgba(166,227,161,0.12); border-color:var(--green); color:var(--green); }
|
||||
#trig-status-badge.waiting { background:rgba(249,226,175,0.12); border-color:var(--yellow); color:var(--yellow); }
|
||||
#trig-status-badge.triggered { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); }
|
||||
#btn-trig-rearm, #btn-trig-stop {
|
||||
#btn-trig-force, #btn-trig-rearm, #btn-trig-stop {
|
||||
border:none; border-radius:5px;
|
||||
padding:4px 12px; font-size:12px; font-weight:600; cursor:pointer; display:none;
|
||||
padding:4px 12px; font-size:12px; font-weight:600; cursor:pointer;
|
||||
}
|
||||
#btn-trig-rearm, #btn-trig-stop { display:none; }
|
||||
#btn-trig-force {
|
||||
background:var(--surface0); color:var(--text);
|
||||
border:1px solid var(--surface1);
|
||||
transition:background var(--transition),border-color var(--transition),color var(--transition);
|
||||
}
|
||||
#btn-trig-force:hover { background:var(--surface1); border-color:var(--mauve); color:var(--mauve); }
|
||||
#btn-trig-rearm { background:var(--mauve); color:var(--crust); }
|
||||
#btn-trig-stop { background:var(--surface1); color:var(--yellow); border:1px solid var(--yellow); }
|
||||
#btn-trig-rearm:hover, #btn-trig-stop:hover { opacity:0.85; }
|
||||
@@ -192,23 +199,6 @@ input[type=range].trig-range::-webkit-slider-thumb {
|
||||
.sig-name { flex:1; font-size:13px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.sig-unit { font-size:11px; color:var(--subtext0); font-style:italic; }
|
||||
.type-badge { font-size:10px; background:var(--surface1); color:var(--subtext1); padding:1px 5px; border-radius:3px; white-space:nowrap; }
|
||||
.array-group {}
|
||||
.array-header {
|
||||
padding:6px 14px 6px 10px; cursor:pointer; border-radius:6px; margin:1px 6px;
|
||||
transition:background var(--transition); display:flex; align-items:center; gap:6px; user-select:none;
|
||||
}
|
||||
.array-header:hover { background:var(--surface0); }
|
||||
.array-arrow { font-size:10px; color:var(--subtext0); transition:transform var(--transition); display:inline-block; }
|
||||
.array-header.open .array-arrow { transform:rotate(90deg); }
|
||||
.array-children { display:none; padding-left:16px; }
|
||||
.array-header.open + .array-children { display:block; }
|
||||
.array-child {
|
||||
padding:4px 14px 4px 8px; cursor:grab; border-radius:6px; margin:1px 6px;
|
||||
transition:background var(--transition); display:flex; align-items:center; gap:8px;
|
||||
user-select:none; color:var(--subtext1); font-size:12px;
|
||||
}
|
||||
.array-child:hover { background:var(--surface0); }
|
||||
.array-child:active { cursor:grabbing; }
|
||||
|
||||
/* ── Main area ────────────────────────────────────────────────── */
|
||||
#main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
|
||||
@@ -324,6 +314,32 @@ input[type=range].trig-range::-webkit-slider-thumb {
|
||||
border:1px solid var(--mauve);
|
||||
}
|
||||
|
||||
/* ── History budget ───────────────────────────────────────────── */
|
||||
#history-badge {
|
||||
font-size:10px; color:var(--yellow); margin-left:8px; cursor:pointer;
|
||||
background:transparent; border:1px solid transparent; border-radius:4px;
|
||||
padding:1px 5px; white-space:nowrap;
|
||||
}
|
||||
#history-badge:hover { border-color:var(--yellow); background:rgba(249,226,175,0.10); }
|
||||
#history-panel {
|
||||
position:fixed; z-index:300;
|
||||
background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius);
|
||||
box-shadow:0 8px 24px rgba(0,0,0,0.6); padding:10px; width:290px;
|
||||
}
|
||||
.hist-note { font-size:10px; color:var(--overlay0); line-height:1.4; margin:6px 0; }
|
||||
.hist-warn { color:var(--peach); }
|
||||
#hist-signal-res {
|
||||
font-size:10px; font-family:monospace; color:var(--subtext0);
|
||||
max-height:120px; overflow-y:auto;
|
||||
border-top:1px solid var(--surface0); border-bottom:1px solid var(--surface0);
|
||||
padding:5px 0;
|
||||
}
|
||||
.hist-res-row { display:flex; justify-content:space-between; gap:8px; }
|
||||
.hist-res-row .hist-res-key {
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--subtext1);
|
||||
}
|
||||
.hist-res-row .hist-res-val { color:var(--mauve); flex-shrink:0; }
|
||||
|
||||
/* ── Signal style context menu ────────────────────────────────── */
|
||||
#sig-ctx-menu {
|
||||
position:fixed; z-index:300;
|
||||
|
||||
Reference in New Issue
Block a user