// 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) { 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}`; } 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; state.timeStep++; 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(); } export function forceRecordSample() { state.timeStep++; 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() { forceRecordSample(); } export function updateWaveInfo() { const totalSamples = Object.values(state.waveData).reduce((sum, arr) => sum + arr.length, 0); document.getElementById('wave-info').textContent = `T=${state.timeStep} | ${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; } // Auto-scroll to show latest const maxVisible = Math.floor(wc.width / state.waveZoom); if (state.timeStep > maxVisible) { state.waveScroll = state.timeStep - maxVisible; } // Draw time grid wctx.strokeStyle = '#151520'; wctx.lineWidth = 1; for (let t = Math.ceil(state.waveScroll); t <= state.timeStep; t++) { const x = (t - state.waveScroll) * state.waveZoom; if (x < 0 || x > wc.width) continue; wctx.beginPath(); wctx.moveTo(x, 0); wctx.lineTo(x, wc.height); wctx.stroke(); if (t % 5 === 0 || state.waveZoom > 30) { wctx.fillStyle = '#333'; wctx.font = '9px monospace'; wctx.textAlign = 'center'; wctx.fillText(`${t}`, 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 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 lastVal = 0; let started = false; // Build complete signal timeline const timeline = []; for (let t = 1; t <= state.timeStep; t++) { const sample = data.filter(s => s.t <= t).pop(); timeline.push(sample ? sample.value : 0); } for (let t = 0; t < timeline.length; t++) { const x = (t + 1 - state.waveScroll) * state.waveZoom; const val = timeline[t]; const y = val ? yHigh : yLow; if (!started) { wctx.moveTo(x, y); started = true; } else { if (val !== lastVal) { wctx.lineTo(x, lastVal ? yHigh : yLow); wctx.lineTo(x, y); } wctx.lineTo(x + state.waveZoom, y); } lastVal = val; } wctx.stroke(); // Fill area under signal wctx.globalAlpha = 0.08; wctx.fillStyle = color; for (let t = 0; t < timeline.length; t++) { const x = (t + 1 - state.waveScroll) * state.waveZoom; if (timeline[t]) { wctx.fillRect(x, yHigh, state.waveZoom, sigH); } } wctx.globalAlpha = 1; }); // Cursor line at current time const cursorX = (state.timeStep - state.waveScroll) * state.waveZoom; 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([]); } }