fix: INPUT toggle no longer disrupts CLK timing in waveform

recordSample() (triggered by user INPUT toggle) now only advances
timeStep when the simulation is stopped. When sim is running, it
records at the current timeStep without advancing it, so the clock's
regular tick cadence is never stolen by manual interactions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 03:19:28 +01:00
parent f0f3516efa
commit d78b45841c

View File

@@ -1,247 +1,258 @@
// Waveform recording and rendering (GTKWave-style) // Waveform recording and rendering (GTKWave-style)
import { SIGNAL_COLORS } from './constants.js'; import { SIGNAL_COLORS } from './constants.js';
import { state } from './state.js'; import { state } from './state.js';
export function getTrackedGates() { export function getTrackedGates() {
const { gates } = state; const { gates } = state;
const clocks = gates.filter(g => g.type === 'CLOCK'); const clocks = gates.filter(g => g.type === 'CLOCK');
const inputs = gates.filter(g => g.type === 'INPUT'); const inputs = gates.filter(g => g.type === 'INPUT');
const outputs = gates.filter(g => g.type === 'OUTPUT'); const outputs = gates.filter(g => g.type === 'OUTPUT');
const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT' && g.type !== 'CLOCK'); const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT' && g.type !== 'CLOCK');
return [...clocks, ...inputs, ...logic, ...outputs]; return [...clocks, ...inputs, ...logic, ...outputs];
} }
export function getGateLabel(gate) { export function getGateLabel(gate) {
const sameType = state.gates.filter(g => g.type === gate.type); const sameType = state.gates.filter(g => g.type === gate.type);
const idx = sameType.indexOf(gate); const idx = sameType.indexOf(gate);
if (gate.type === 'CLOCK') return `CLK_${idx}`; if (gate.type === 'CLOCK') return `CLK_${idx}`;
if (gate.type === 'INPUT') return `IN_${idx}`; if (gate.type === 'INPUT') return `IN_${idx}`;
if (gate.type === 'OUTPUT') return `OUT_${idx}`; if (gate.type === 'OUTPUT') return `OUT_${idx}`;
return `${gate.type}_${idx}`; return `${gate.type}_${idx}`;
} }
export function recordSample() { /**
const { gates, waveData } = state; * Record a sample triggered by user interaction (INPUT toggle).
* When sim is running, records at current timeStep WITHOUT advancing it.
const changed = gates.some(g => { * When sim is stopped, advances timeStep first.
const data = waveData[g.id]; */
if (!data || data.length === 0) return true; export function recordSample() {
return data[data.length - 1].value !== g.value; const { gates, waveData } = state;
});
const changed = gates.some(g => {
if (!changed && state.timeStep > 0) return; const data = waveData[g.id];
if (!data || data.length === 0) return true;
// Manual toggles advance by simSpeed too for consistency return data[data.length - 1].value !== g.value;
state.timeStep += state.simSpeed; });
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = []; if (!changed && state.timeStep > 0) return;
const arr = waveData[g.id];
if (arr.length === 0 || arr[arr.length - 1].value !== g.value) { // Only advance time if sim is NOT running (manual mode)
arr.push({ t: state.timeStep, value: g.value }); if (!state.simRunning) {
} state.timeStep += state.simSpeed;
}); }
updateWaveInfo();
} gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
export function forceRecordSample() { const arr = waveData[g.id];
// Advance time by the current simSpeed (in ms) to reflect real time if (arr.length === 0 || arr[arr.length - 1].value !== g.value) {
state.timeStep += state.simSpeed; arr.push({ t: state.timeStep, value: g.value });
state.gates.forEach(g => { }
if (!state.waveData[g.id]) state.waveData[g.id] = []; });
state.waveData[g.id].push({ t: state.timeStep, value: g.value }); updateWaveInfo();
}); }
updateWaveInfo();
} /**
* Record a sample from the simulation tick.
export function manualStep() { * Always advances timeStep — this is the ONLY source of time when sim is running.
// Toggle all CLOCK gates on each step (same as simTick) */
state.gates.forEach(g => { export function forceRecordSample() {
if (g.type === 'CLOCK') { state.timeStep += state.simSpeed;
g.value = g.value ? 0 : 1; state.gates.forEach(g => {
} if (!state.waveData[g.id]) state.waveData[g.id] = [];
}); state.waveData[g.id].push({ t: state.timeStep, value: g.value });
// Use the lazy-loaded evaluateAll to avoid circular imports });
if (_evaluateAll) _evaluateAll(); updateWaveInfo();
forceRecordSample(); }
}
export function manualStep() {
// Lazy reference to evaluateAll (set by gates.js to break circular dep) // Toggle all CLOCK gates on each step (same as simTick)
let _evaluateAll = null; state.gates.forEach(g => {
export function setEvaluateAll(fn) { if (g.type === 'CLOCK') {
_evaluateAll = fn; g.value = g.value ? 0 : 1;
} }
});
export function updateWaveInfo() { // Use the lazy-loaded evaluateAll to avoid circular imports
const totalSamples = Object.values(state.waveData).reduce((sum, arr) => sum + arr.length, 0); if (_evaluateAll) _evaluateAll();
const timeLabel = state.timeStep >= 1000 forceRecordSample();
? `${(state.timeStep/1000).toFixed(1)}s` }
: `${state.timeStep}ms`;
document.getElementById('wave-info').textContent = `T=${timeLabel} | ${totalSamples} samples`; // Lazy reference to evaluateAll (set by gates.js to break circular dep)
} let _evaluateAll = null;
export function setEvaluateAll(fn) {
export function clearWaveData() { _evaluateAll = fn;
state.waveData = {}; }
state.timeStep = 0;
state.waveScroll = 0; export function updateWaveInfo() {
updateWaveInfo(); const totalSamples = Object.values(state.waveData).reduce((sum, arr) => sum + arr.length, 0);
} const timeLabel = state.timeStep >= 1000
? `${(state.timeStep/1000).toFixed(1)}s`
export function drawWaveLabels() { : `${state.timeStep}ms`;
const labelsEl = document.getElementById('wave-labels'); document.getElementById('wave-info').textContent = `T=${timeLabel} | ${totalSamples} samples`;
labelsEl.innerHTML = ''; }
const tracked = getTrackedGates();
tracked.forEach((gate, i) => { export function clearWaveData() {
const div = document.createElement('div'); state.waveData = {};
div.className = 'wave-label'; state.timeStep = 0;
if (gate.type === 'INPUT') div.classList.add('input-label'); state.waveScroll = 0;
else if (gate.type === 'OUTPUT') div.classList.add('output-label'); updateWaveInfo();
else div.classList.add('gate-label'); }
div.textContent = getGateLabel(gate);
div.style.color = SIGNAL_COLORS[i % SIGNAL_COLORS.length]; export function drawWaveLabels() {
labelsEl.appendChild(div); const labelsEl = document.getElementById('wave-labels');
}); labelsEl.innerHTML = '';
} const tracked = getTrackedGates();
tracked.forEach((gate, i) => {
export function drawWaveforms() { const div = document.createElement('div');
const wc = document.getElementById('wave-canvas'); div.className = 'wave-label';
const wctx = wc.getContext('2d'); if (gate.type === 'INPUT') div.classList.add('input-label');
const container = document.getElementById('wave-container'); else if (gate.type === 'OUTPUT') div.classList.add('output-label');
else div.classList.add('gate-label');
wc.width = container.clientWidth - 100; div.textContent = getGateLabel(gate);
wc.height = container.clientHeight; div.style.color = SIGNAL_COLORS[i % SIGNAL_COLORS.length];
labelsEl.appendChild(div);
wctx.fillStyle = '#0c0c14'; });
wctx.fillRect(0, 0, wc.width, wc.height); }
const tracked = getTrackedGates(); export function drawWaveforms() {
const rowH = 30; const wc = document.getElementById('wave-canvas');
const sigH = 20; const wctx = wc.getContext('2d');
const margin = (rowH - sigH) / 2; const container = document.getElementById('wave-container');
if (state.timeStep === 0) { wc.width = container.clientWidth - 100;
wctx.fillStyle = '#333'; wc.height = container.clientHeight;
wctx.font = '12px "Segoe UI", system-ui';
wctx.textAlign = 'center'; wctx.fillStyle = '#0c0c14';
wctx.fillText('Toggle inputs to record signals...', wc.width / 2, wc.height / 2); wctx.fillRect(0, 0, wc.width, wc.height);
return;
} const tracked = getTrackedGates();
const rowH = 30;
// pxPerMs: how many pixels per millisecond of simulation time const sigH = 20;
const pxPerMs = state.waveZoom / 100; // waveZoom=20 → 0.2 px/ms const margin = (rowH - sigH) / 2;
// Total width in pixels for all recorded time if (state.timeStep === 0) {
const totalPx = state.timeStep * pxPerMs; wctx.fillStyle = '#333';
wctx.font = '12px "Segoe UI", system-ui';
// Visible width in ms wctx.textAlign = 'center';
const visibleMs = wc.width / pxPerMs; wctx.fillText('Toggle inputs to record signals...', wc.width / 2, wc.height / 2);
return;
// Auto-scroll: always follow the latest data, keep cursor at right edge }
state.waveScroll = Math.max(0, state.timeStep - visibleMs);
// pxPerMs: how many pixels per millisecond of simulation time
// Helper: convert simulation time (ms) to pixel X const pxPerMs = state.waveZoom / 100; // waveZoom=20 → 0.2 px/ms
const tToX = (t) => (t - state.waveScroll) * pxPerMs;
// Total width in pixels for all recorded time
// Draw time grid (every gridMs milliseconds) const totalPx = state.timeStep * pxPerMs;
let gridMs = 500;
if (pxPerMs * gridMs < 30) gridMs = 1000; // Visible width in ms
if (pxPerMs * gridMs < 30) gridMs = 2000; const visibleMs = wc.width / pxPerMs;
if (pxPerMs * gridMs > 200) gridMs = 200;
if (pxPerMs * gridMs > 200) gridMs = 100; // Auto-scroll: always follow the latest data, keep cursor at right edge
state.waveScroll = Math.max(0, state.timeStep - visibleMs);
wctx.strokeStyle = '#151520';
wctx.lineWidth = 1; // Helper: convert simulation time (ms) to pixel X
const startT = Math.floor(state.waveScroll / gridMs) * gridMs; const tToX = (t) => (t - state.waveScroll) * pxPerMs;
for (let t = startT; t <= state.timeStep; t += gridMs) {
const x = tToX(t); // Draw time grid (every gridMs milliseconds)
if (x < 0 || x > wc.width) continue; let gridMs = 500;
wctx.beginPath(); if (pxPerMs * gridMs < 30) gridMs = 1000;
wctx.moveTo(x, 0); if (pxPerMs * gridMs < 30) gridMs = 2000;
wctx.lineTo(x, wc.height); if (pxPerMs * gridMs > 200) gridMs = 200;
wctx.stroke(); if (pxPerMs * gridMs > 200) gridMs = 100;
wctx.fillStyle = '#333'; wctx.strokeStyle = '#151520';
wctx.font = '9px monospace'; wctx.lineWidth = 1;
wctx.textAlign = 'center'; const startT = Math.floor(state.waveScroll / gridMs) * gridMs;
const label = t >= 1000 ? `${(t/1000).toFixed(1)}s` : `${t}ms`; for (let t = startT; t <= state.timeStep; t += gridMs) {
wctx.fillText(label, x, 10); const x = tToX(t);
} if (x < 0 || x > wc.width) continue;
wctx.beginPath();
// Row dividers wctx.moveTo(x, 0);
tracked.forEach((_, i) => { wctx.lineTo(x, wc.height);
const y = i * rowH + rowH; wctx.stroke();
wctx.strokeStyle = '#111118';
wctx.beginPath(); wctx.fillStyle = '#333';
wctx.moveTo(0, y); wctx.font = '9px monospace';
wctx.lineTo(wc.width, y); wctx.textAlign = 'center';
wctx.stroke(); const label = t >= 1000 ? `${(t/1000).toFixed(1)}s` : `${t}ms`;
}); wctx.fillText(label, x, 10);
}
// Draw signals using actual timestamps
tracked.forEach((gate, i) => { // Row dividers
const data = state.waveData[gate.id] || []; tracked.forEach((_, i) => {
if (data.length === 0) return; const y = i * rowH + rowH;
wctx.strokeStyle = '#111118';
const color = SIGNAL_COLORS[i % SIGNAL_COLORS.length]; wctx.beginPath();
const y0 = i * rowH + margin; wctx.moveTo(0, y);
const yHigh = y0 + 2; wctx.lineTo(wc.width, y);
const yLow = y0 + sigH; wctx.stroke();
});
wctx.strokeStyle = color;
wctx.lineWidth = 1.5; // Draw signals using actual timestamps
wctx.beginPath(); tracked.forEach((gate, i) => {
const data = state.waveData[gate.id] || [];
let started = false; if (data.length === 0) return;
for (let s = 0; s < data.length; s++) { const color = SIGNAL_COLORS[i % SIGNAL_COLORS.length];
const sample = data[s]; const y0 = i * rowH + margin;
const nextT = s < data.length - 1 ? data[s + 1].t : state.timeStep; const yHigh = y0 + 2;
const x1 = tToX(sample.t); const yLow = y0 + sigH;
const x2 = tToX(nextT);
const y = sample.value ? yHigh : yLow; wctx.strokeStyle = color;
wctx.lineWidth = 1.5;
if (!started) { wctx.beginPath();
// Draw from time 0 to first sample
const x0 = tToX(0); let started = false;
const initY = yLow; // default low before first sample
if (x0 < wc.width) { for (let s = 0; s < data.length; s++) {
wctx.moveTo(Math.max(0, x0), initY); const sample = data[s];
if (sample.t > 0) wctx.lineTo(x1, initY); const nextT = s < data.length - 1 ? data[s + 1].t : state.timeStep;
wctx.lineTo(x1, y); const x1 = tToX(sample.t);
} const x2 = tToX(nextT);
started = true; const y = sample.value ? yHigh : yLow;
} else {
// Vertical transition from previous value if (!started) {
const prevVal = data[s - 1].value; // Draw from time 0 to first sample
if (sample.value !== prevVal) { const x0 = tToX(0);
wctx.lineTo(x1, prevVal ? yHigh : yLow); const initY = yLow; // default low before first sample
wctx.lineTo(x1, y); if (x0 < wc.width) {
} wctx.moveTo(Math.max(0, x0), initY);
} if (sample.t > 0) wctx.lineTo(x1, initY);
// Horizontal line to next transition wctx.lineTo(x1, y);
wctx.lineTo(x2, y); }
started = true;
// Fill high regions } else {
if (sample.value) { // Vertical transition from previous value
wctx.save(); const prevVal = data[s - 1].value;
wctx.globalAlpha = 0.08; if (sample.value !== prevVal) {
wctx.fillStyle = color; wctx.lineTo(x1, prevVal ? yHigh : yLow);
wctx.fillRect(x1, yHigh, x2 - x1, sigH); wctx.lineTo(x1, y);
wctx.restore(); }
} }
} // Horizontal line to next transition
wctx.stroke(); wctx.lineTo(x2, y);
});
// Fill high regions
// Cursor line at current time if (sample.value) {
const cursorX = tToX(state.timeStep); wctx.save();
if (cursorX >= 0 && cursorX <= wc.width) { wctx.globalAlpha = 0.08;
wctx.strokeStyle = '#00e59966'; wctx.fillStyle = color;
wctx.lineWidth = 1; wctx.fillRect(x1, yHigh, x2 - x1, sigH);
wctx.setLineDash([4, 3]); wctx.restore();
wctx.beginPath(); }
wctx.moveTo(cursorX, 0); }
wctx.lineTo(cursorX, wc.height); wctx.stroke();
wctx.stroke(); });
wctx.setLineDash([]);
} // Cursor line at current time
} const cursorX = tToX(state.timeStep);
if (cursorX >= 0 && cursorX <= wc.width) {
wctx.strokeStyle = '#00e59966';
wctx.lineWidth = 1;
wctx.setLineDash([4, 3]);
wctx.beginPath();
wctx.moveTo(cursorX, 0);
wctx.lineTo(cursorX, wc.height);
wctx.stroke();
wctx.setLineDash([]);
}
}