Fix scope viewer: initialize div state from data range to prevent empty CRT

xDiv/yDiv were initialized as null and excluded from the main uPlot
effect's dependency array, causing the effect to bail on its null guard
and never re-run when the init effect set them to computed values.
This commit is contained in:
Ryan Malloy 2026-02-16 23:27:46 -07:00
parent ac82068b98
commit 3c9c83742d
2 changed files with 20 additions and 19 deletions

View File

@ -104,18 +104,8 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
return waveform.variables.length > 1 ? waveform.variables[1].type : 'voltage'; return waveform.variables.length > 1 ? waveform.variables[1].type : 'voltage';
}, [waveform, isAC]); }, [waveform, isAC]);
// State
const [activeTrace, setActiveTrace] = useState(0);
const [xDiv, setXDiv] = useState<number | null>(null);
const [yDiv, setYDiv] = useState<number | null>(null);
const [skinId, setSkinId] = useState<SkinId>(() => {
const stored = safeGetItem('spicebook-scope-skin') as SkinId | null;
return stored && SKINS[stored] ? stored : '465';
});
const skin = SKINS[skinId];
// Compute data range using loop-based min/max (safe for large arrays) // Compute data range using loop-based min/max (safe for large arrays)
// Must be computed before useState so div initializers can reference them
const { xMin, xMax, yMin, yMax } = useMemo(() => { const { xMin, xMax, yMin, yMax } = useMemo(() => {
const [xLo, xHi] = arrayMinMax(waveform.x_data); const [xLo, xHi] = arrayMinMax(waveform.x_data);
const yData = isAC && waveform.y_magnitude_db const yData = isAC && waveform.y_magnitude_db
@ -125,7 +115,18 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
return { xMin: xLo, xMax: xHi, yMin: yLo, yMax: yHi }; return { xMin: xLo, xMax: xHi, yMin: yLo, yMax: yHi };
}, [waveform, isAC]); }, [waveform, isAC]);
// Initialize div values from data range // State — xDiv/yDiv initialized from data range (never null)
const [activeTrace, setActiveTrace] = useState(0);
const [xDiv, setXDiv] = useState(() => autoDiv(xMin, xMax, NUM_H_DIVS, xSeqForType(xType)));
const [yDiv, setYDiv] = useState(() => autoDiv(yMin, yMax, NUM_V_DIVS, ySeqForType(yType)));
const [skinId, setSkinId] = useState<SkinId>(() => {
const stored = safeGetItem('spicebook-scope-skin') as SkinId | null;
return stored && SKINS[stored] ? stored : '465';
});
const skin = SKINS[skinId];
// Re-initialize div values when data range changes (new waveform loaded)
useEffect(() => { useEffect(() => {
setXDiv(autoDiv(xMin, xMax, NUM_H_DIVS, xSeqForType(xType))); setXDiv(autoDiv(xMin, xMax, NUM_H_DIVS, xSeqForType(xType)));
setYDiv(autoDiv(yMin, yMax, NUM_V_DIVS, ySeqForType(yType))); setYDiv(autoDiv(yMin, yMax, NUM_V_DIVS, ySeqForType(yType)));
@ -133,7 +134,7 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
// Build and mount uPlot (only on data/size changes, NOT on trace/div changes) // Build and mount uPlot (only on data/size changes, NOT on trace/div changes)
useEffect(() => { useEffect(() => {
if (!screenRef.current || width <= 0 || xDiv === null || yDiv === null) return; if (!screenRef.current || width <= 0) return;
const el = screenRef.current; const el = screenRef.current;
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
@ -219,7 +220,7 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
// Update X/Y scale via setScale (no chart rebuild) // Update X/Y scale via setScale (no chart rebuild)
useEffect(() => { useEffect(() => {
const u = uplotRef.current; const u = uplotRef.current;
if (!u || xDiv === null || yDiv === null) return; if (!u) return;
const xMid = (xMin + xMax) / 2; const xMid = (xMin + xMax) / 2;
const yMid = (yMin + yMax) / 2; const yMid = (yMin + yMax) / 2;
@ -240,7 +241,6 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
const stepXDiv = useCallback(() => { const stepXDiv = useCallback(() => {
setXDiv(prev => { setXDiv(prev => {
if (prev === null) return prev;
const seq = xSeqForType(xType); const seq = xSeqForType(xType);
const next = step125(prev, seq, 1); const next = step125(prev, seq, 1);
if (next === prev) return seq[0]; if (next === prev) return seq[0];
@ -250,7 +250,6 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
const stepYDiv = useCallback(() => { const stepYDiv = useCallback(() => {
setYDiv(prev => { setYDiv(prev => {
if (prev === null) return prev;
const seq = ySeqForType(yType); const seq = ySeqForType(yType);
const next = step125(prev, seq, 1); const next = step125(prev, seq, 1);
if (next === prev) return seq[0]; if (next === prev) return seq[0];
@ -273,8 +272,8 @@ export function ScopeWaveformViewer({ waveform, width, onExitScope }: ScopeWavef
: 0; : 0;
const currentTraceName = traceNames[activeTrace] || '\u2014'; const currentTraceName = traceNames[activeTrace] || '\u2014';
const xDivLabel = xDiv !== null ? formatEng(xDiv, divUnit(xType) + '/div') : '\u2014'; const xDivLabel = formatEng(xDiv, divUnit(xType) + '/div');
const yDivLabel = yDiv !== null ? formatEng(yDiv, divUnit(yType) + '/div') : '\u2014'; const yDivLabel = formatEng(yDiv, divUnit(yType) + '/div');
return ( return (
<div className="scope-skin-active"> <div className="scope-skin-active">

View File

@ -125,8 +125,10 @@
/* uPlot chart fills the CRT screen */ /* uPlot chart fills the CRT screen */
.scope-skin-active .scope-screen .uplot { .scope-skin-active .scope-screen .uplot {
position: absolute; position: absolute !important;
inset: 0; inset: 0;
width: 100% !important;
height: 100% !important;
} }
/* ── Graticule overlay (10 horizontal × 8 vertical) ──────── */ /* ── Graticule overlay (10 horizontal × 8 vertical) ──────── */