[fix](trx-frontend): fix spectrum screenshot hotkey

Preserve the WebGL drawing buffers used by the spectrum snapshot,
flush them before compositing, and move the shortcut listener to
capture phase so focused widgets do not swallow it.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-17 23:11:10 +01:00
parent 9019acee0e
commit 2517ed0b29
@@ -339,11 +339,13 @@ const connLostOverlayTitleEl = document.getElementById("conn-lost-overlay-title"
const connLostOverlaySubEl = document.getElementById("conn-lost-overlay-sub"); const connLostOverlaySubEl = document.getElementById("conn-lost-overlay-sub");
const overviewCanvas = document.getElementById("overview-canvas"); const overviewCanvas = document.getElementById("overview-canvas");
const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); const signalOverlayCanvas = document.getElementById("signal-overlay-canvas");
// Screenshots composite these live WebGL canvases into a PNG.
const spectrumSnapshotGlOptions = { alpha: true, preserveDrawingBuffer: true };
const overviewGl = typeof createTrxWebGlRenderer === "function" const overviewGl = typeof createTrxWebGlRenderer === "function"
? createTrxWebGlRenderer(overviewCanvas, { alpha: true }) ? createTrxWebGlRenderer(overviewCanvas, spectrumSnapshotGlOptions)
: null; : null;
const signalOverlayGl = typeof createTrxWebGlRenderer === "function" const signalOverlayGl = typeof createTrxWebGlRenderer === "function"
? createTrxWebGlRenderer(signalOverlayCanvas, { alpha: true }) ? createTrxWebGlRenderer(signalOverlayCanvas, spectrumSnapshotGlOptions)
: null; : null;
const signalVisualBlockEl = document.querySelector(".signal-visual-block"); const signalVisualBlockEl = document.querySelector(".signal-visual-block");
const signalSplitControlEl = document.getElementById("signal-split-control"); const signalSplitControlEl = document.getElementById("signal-split-control");
@@ -7920,7 +7922,7 @@ window.addEventListener("beforeunload", () => {
// ── Spectrum display ───────────────────────────────────────────────────────── // ── Spectrum display ─────────────────────────────────────────────────────────
const spectrumCanvas = document.getElementById("spectrum-canvas"); const spectrumCanvas = document.getElementById("spectrum-canvas");
const spectrumGl = typeof createTrxWebGlRenderer === "function" const spectrumGl = typeof createTrxWebGlRenderer === "function"
? createTrxWebGlRenderer(spectrumCanvas, { alpha: true }) ? createTrxWebGlRenderer(spectrumCanvas, spectrumSnapshotGlOptions)
: null; : null;
const spectrumDbAxis = document.getElementById("spectrum-db-axis"); const spectrumDbAxis = document.getElementById("spectrum-db-axis");
const spectrumFreqAxis = document.getElementById("spectrum-freq-axis"); const spectrumFreqAxis = document.getElementById("spectrum-freq-axis");
@@ -9103,6 +9105,16 @@ function buildSpectrumSnapshotCanvas() {
if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) { if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) {
return null; return null;
} }
for (const renderer of [overviewGl, spectrumGl, signalOverlayGl]) {
const gl = renderer?.gl;
if (!gl) continue;
try {
if (typeof gl.flush === "function") gl.flush();
if (typeof gl.finish === "function") gl.finish();
} catch (_) {
// Ignore transient WebGL state errors and capture the last good frame.
}
}
const rootRect = rootEl.getBoundingClientRect(); const rootRect = rootEl.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
const out = document.createElement("canvas"); const out = document.createElement("canvas");
@@ -9153,35 +9165,55 @@ function buildSpectrumSnapshotCanvas() {
return out; return out;
} }
function saveCanvasAsPng(canvas, fileName) { function clickCanvasDownload(href, fileName) {
if (!canvas) return;
if (typeof canvas.toBlob === "function") {
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = href;
a.download = fileName;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}, "image/png");
return;
}
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = fileName; a.download = fileName;
a.rel = "noopener";
a.style.display = "none";
document.body.appendChild(a);
a.click(); a.click();
requestAnimationFrame(() => a.remove());
} }
function captureSpectrumScreenshot() { function saveCanvasAsPng(canvas, fileName) {
if (!canvas) return Promise.resolve(false);
if (typeof canvas.toBlob === "function") {
return new Promise((resolve) => {
try {
canvas.toBlob((blob) => {
if (!blob) {
resolve(false);
return;
}
const url = URL.createObjectURL(blob);
clickCanvasDownload(url, fileName);
setTimeout(() => URL.revokeObjectURL(url), 1000);
resolve(true);
}, "image/png");
} catch (_) {
resolve(false);
}
});
}
try {
clickCanvasDownload(canvas.toDataURL("image/png"), fileName);
return Promise.resolve(true);
} catch (_) {
return Promise.resolve(false);
}
}
async function captureSpectrumScreenshot() {
const snapshotCanvas = buildSpectrumSnapshotCanvas(); const snapshotCanvas = buildSpectrumSnapshotCanvas();
if (!snapshotCanvas) { if (!snapshotCanvas) {
showHint("Spectrum view not ready", 1300); showHint("Spectrum view not ready", 1300);
return; return false;
} }
const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const stamp = new Date().toISOString().replace(/[:.]/g, "-");
saveCanvasAsPng(snapshotCanvas, `trx-spectrum-${stamp}.png`); const saved = await saveCanvasAsPng(snapshotCanvas, `trx-spectrum-${stamp}.png`);
showHint("Spectrum screenshot saved", 1500); showHint(saved ? "Spectrum screenshot saved" : "Spectrum screenshot failed", saved ? 1500 : 1800);
return saved;
} }
function shouldIgnoreGlobalShortcut(target) { function shouldIgnoreGlobalShortcut(target) {
@@ -9193,13 +9225,13 @@ function shouldIgnoreGlobalShortcut(target) {
} }
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
if (event.defaultPrevented || event.repeat) return; if (event.defaultPrevented || event.repeat || event.isComposing) return;
if (event.ctrlKey || event.metaKey || event.altKey) return; if (event.ctrlKey || event.metaKey || event.altKey) return;
if (shouldIgnoreGlobalShortcut(event.target)) return; if (shouldIgnoreGlobalShortcut(event.target)) return;
if ((event.key || "").toLowerCase() !== "s") return; if ((event.key || "").toLowerCase() !== "s") return;
event.preventDefault(); event.preventDefault();
captureSpectrumScreenshot(); void captureSpectrumScreenshot();
}); }, { capture: true });
// ── Zoom helpers ────────────────────────────────────────────────────────────── // ── Zoom helpers ──────────────────────────────────────────────────────────────
function spectrumZoomAt(cssX, cssW, data, factor) { function spectrumZoomAt(cssX, cssW, data, factor) {