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