Files
logic-gates/js/waveform.js

230 lines
7.2 KiB
JavaScript

// 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() {
// 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);
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 (only if we're already near the end)
const maxVisible = Math.floor(wc.width / state.waveZoom);
const isNearEnd = state.waveScroll >= state.timeStep - maxVisible - 2;
if (state.timeStep > maxVisible && isNearEnd) {
state.waveScroll = state.timeStep - maxVisible;
}
// Clamp scroll to valid range
state.waveScroll = Math.max(0, Math.min(state.timeStep - 1, state.waveScroll));
// 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([]);
}
}