Add CLOCK gate with automatic simulation

- CLOCK gate auto-toggles 0→1→0 during simulation
- Run/Stop button in waveform toolbar
- Adjustable speed (50ms to 2000ms per tick)
- Click CLOCK to toggle manually when sim is stopped
- Waveform auto-opens when sim starts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis Montañes
2026-03-19 19:42:46 +01:00
parent 41f15c474b
commit 6fbc6e4896

View File

@@ -50,6 +50,8 @@
.gate-btn:hover { background: #252540; border-color: #00e599; color: #fff; } .gate-btn:hover { background: #252540; border-color: #00e599; color: #fff; }
.gate-btn.input-btn { border-color: #3388ff; } .gate-btn.input-btn { border-color: #3388ff; }
.gate-btn.input-btn:hover { border-color: #55aaff; } .gate-btn.input-btn:hover { border-color: #55aaff; }
.gate-btn.clock-btn { border-color: #ff44aa; }
.gate-btn.clock-btn:hover { border-color: #ff66cc; }
.gate-btn.output-btn { border-color: #ff8833; } .gate-btn.output-btn { border-color: #ff8833; }
.gate-btn.output-btn:hover { border-color: #ffaa55; } .gate-btn.output-btn:hover { border-color: #ffaa55; }
.separator { width: 1px; height: 24px; background: #2a2a3a; margin: 0 6px; } .separator { width: 1px; height: 24px; background: #2a2a3a; margin: 0 6px; }
@@ -229,6 +231,7 @@
<div id="toolbar"> <div id="toolbar">
<span class="logo">⚡ Logic Lab</span> <span class="logo">⚡ Logic Lab</span>
<button class="gate-btn input-btn" data-gate="INPUT">INPUT</button> <button class="gate-btn input-btn" data-gate="INPUT">INPUT</button>
<button class="gate-btn clock-btn" data-gate="CLOCK">CLOCK</button>
<button class="gate-btn output-btn" data-gate="OUTPUT">OUTPUT</button> <button class="gate-btn output-btn" data-gate="OUTPUT">OUTPUT</button>
<div class="separator"></div> <div class="separator"></div>
<button class="gate-btn" data-gate="AND">AND</button> <button class="gate-btn" data-gate="AND">AND</button>
@@ -256,6 +259,11 @@
<button class="wave-btn" id="wave-step">Step ▶</button> <button class="wave-btn" id="wave-step">Step ▶</button>
<button class="wave-btn" id="wave-zoom-in">Zoom +</button> <button class="wave-btn" id="wave-zoom-in">Zoom +</button>
<button class="wave-btn" id="wave-zoom-out">Zoom -</button> <button class="wave-btn" id="wave-zoom-out">Zoom -</button>
<div class="separator"></div>
<button class="wave-btn" id="sim-run-btn">▶ Run</button>
<button class="wave-btn" id="sim-slower"></button>
<span class="wave-info" id="sim-speed-label" style="margin-left:0">500ms</span>
<button class="wave-btn" id="sim-faster">+</button>
<span class="wave-info" id="wave-info">T=0 | 0 samples</span> <span class="wave-info" id="wave-info">T=0 | 0 samples</span>
</div> </div>
<div id="wave-container"> <div id="wave-container">
@@ -276,6 +284,13 @@
<li>Right-click a port to delete its connections</li> <li>Right-click a port to delete its connections</li>
<li><kbd>Escape</kbd> to cancel placing/connecting</li> <li><kbd>Escape</kbd> to cancel placing/connecting</li>
</ul> </ul>
<h3>Clock &amp; Simulation</h3>
<ul>
<li>Place a <kbd>CLOCK</kbd> gate — it auto-toggles 0/1 during simulation</li>
<li>Click <kbd>▶ Run</kbd> in the waveform bar to start the clock</li>
<li>Use <kbd></kbd> / <kbd>+</kbd> to adjust speed (50ms2000ms per tick)</li>
<li>Connect CLOCK to gates to see automatic signal propagation</li>
</ul>
<h3>Waveform Viewer (GTKWave-style)</h3> <h3>Waveform Viewer (GTKWave-style)</h3>
<ul> <ul>
<li>Click <kbd>📊 Waveform</kbd> to toggle the signal viewer</li> <li>Click <kbd>📊 Waveform</kbd> to toggle the signal viewer</li>
@@ -283,7 +298,7 @@
<li><kbd>Step ▶</kbd> manually advances one time step</li> <li><kbd>Step ▶</kbd> manually advances one time step</li>
<li>Toggle inputs to see signals change in real-time</li> <li>Toggle inputs to see signals change in real-time</li>
<li>Drag the top border to resize the panel</li> <li>Drag the top border to resize the panel</li>
<li>All INPUT, OUTPUT, and gate signals are tracked</li> <li>All CLOCK, INPUT, OUTPUT, and gate signals are tracked</li>
</ul> </ul>
<p style="margin-top: 12px; color: #666;">Built with ❤️ at MontLab</p> <p style="margin-top: 12px; color: #666;">Built with ❤️ at MontLab</p>
<button id="help-close">Got it</button> <button id="help-close">Got it</button>
@@ -312,7 +327,7 @@
const GATE_COLORS = { const GATE_COLORS = {
AND: '#00e599', OR: '#3388ff', NOT: '#ff6644', AND: '#00e599', OR: '#3388ff', NOT: '#ff6644',
NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa', NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa',
INPUT: '#3388ff', OUTPUT: '#ff8833' INPUT: '#3388ff', CLOCK: '#ff44aa', OUTPUT: '#ff8833'
}; };
const SIGNAL_COLORS = [ const SIGNAL_COLORS = [
@@ -331,8 +346,14 @@
let waveScroll = 0; let waveScroll = 0;
let resizingWave = false; let resizingWave = false;
// Clock / simulation state
let simRunning = false;
let simInterval = null;
let simSpeed = 500; // ms per tick
function gateInputCount(type) { function gateInputCount(type) {
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1; if (type === 'CLOCK' || type === 'INPUT') return 0;
if (type === 'NOT' || type === 'OUTPUT') return 1;
return 2; return 2;
} }
function gateOutputCount(type) { function gateOutputCount(type) {
@@ -342,7 +363,7 @@
function evaluate(gate, visited = new Set()) { function evaluate(gate, visited = new Set()) {
if (visited.has(gate.id)) return gate.value || 0; if (visited.has(gate.id)) return gate.value || 0;
visited.add(gate.id); visited.add(gate.id);
if (gate.type === 'INPUT') return gate.value; if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value;
const inputCount = gateInputCount(gate.type); const inputCount = gateInputCount(gate.type);
const inputs = []; const inputs = [];
@@ -371,7 +392,7 @@
} }
function evaluateAll() { function evaluateAll() {
gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; }); gates.forEach(g => { if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0; });
gates.forEach(g => evaluate(g)); gates.forEach(g => evaluate(g));
if (recording && waveformVisible) recordSample(); if (recording && waveformVisible) recordSample();
} }
@@ -413,16 +434,17 @@
} }
function getTrackedGates() { function getTrackedGates() {
// Order: inputs first, then gates, then outputs 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'); const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT' && g.type !== 'CLOCK');
return [...inputs, ...logic, ...outputs]; return [...clocks, ...inputs, ...logic, ...outputs];
} }
function getGateLabel(gate) { function getGateLabel(gate) {
const sameType = gates.filter(g => g.type === gate.type); const sameType = 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 === '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}`;
@@ -633,15 +655,16 @@
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
const label = getGateLabel(gate); const label = getGateLabel(gate);
ctx.fillText(gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (gate.type === 'INPUT' || gate.type === 'OUTPUT' ? 8 : 0)); const isIOType = gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK';
ctx.fillText(gate.type === 'CLOCK' ? '⏱ CLK' : gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (isIOType ? 8 : 0));
// Small ID label // Small ID label
ctx.font = '9px monospace'; ctx.font = '9px monospace';
ctx.fillStyle = '#444'; ctx.fillStyle = '#444';
ctx.fillText(label, gate.x + GATE_W / 2, gate.y + GATE_H - 6); ctx.fillText(label, gate.x + GATE_W / 2, gate.y + GATE_H - 6);
// Value for INPUT/OUTPUT // Value for INPUT/OUTPUT/CLOCK
if (gate.type === 'INPUT' || gate.type === 'OUTPUT') { if (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK') {
ctx.font = 'bold 16px monospace'; ctx.font = 'bold 16px monospace';
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444'; ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 10); ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 10);
@@ -819,7 +842,7 @@
if (connecting) { connecting = null; return; } if (connecting) { connecting = null; return; }
const gate = findGateAt(x, y); const gate = findGateAt(x, y);
if (gate && gate.type === 'INPUT') { if (gate && (gate.type === 'INPUT' || gate.type === 'CLOCK')) {
gate.value = gate.value ? 0 : 1; gate.value = gate.value ? 0 : 1;
evaluateAll(); evaluateAll();
return; return;
@@ -924,6 +947,77 @@
waveZoom = Math.max(5, waveZoom - 5); waveZoom = Math.max(5, waveZoom - 5);
}); });
// ==================== CLOCK SIMULATION ====================
function simTick() {
// Toggle all CLOCK gates
gates.forEach(g => {
if (g.type === 'CLOCK') {
g.value = g.value ? 0 : 1;
}
});
evaluateAll();
// Force record even if evaluateAll didn't detect change
if (recording && waveformVisible) {
timeStep++;
gates.forEach(g => {
if (!waveData[g.id]) waveData[g.id] = [];
waveData[g.id].push({ t: timeStep, value: g.value });
});
updateWaveInfo();
}
}
function startSim() {
if (simRunning) return;
const hasClocks = gates.some(g => g.type === 'CLOCK');
if (!hasClocks) { alert('Place a CLOCK gate first!'); return; }
simRunning = true;
// Auto-open waveform panel
if (!waveformVisible) {
waveformVisible = true;
document.getElementById('waveform-panel').classList.add('visible');
document.getElementById('sim-btn').classList.add('active');
resize();
}
simInterval = setInterval(simTick, simSpeed);
updateSimUI();
}
function stopSim() {
simRunning = false;
if (simInterval) clearInterval(simInterval);
simInterval = null;
updateSimUI();
}
function updateSimUI() {
const btn = document.getElementById('sim-run-btn');
if (simRunning) {
btn.textContent = '⏹ Stop';
btn.classList.add('active');
} else {
btn.textContent = '▶ Run';
btn.classList.remove('active');
}
document.getElementById('sim-speed-label').textContent = `${simSpeed}ms`;
}
document.getElementById('sim-run-btn').addEventListener('click', () => {
if (simRunning) stopSim(); else startSim();
});
document.getElementById('sim-faster').addEventListener('click', () => {
simSpeed = Math.max(50, simSpeed - 100);
if (simRunning) { clearInterval(simInterval); simInterval = setInterval(simTick, simSpeed); }
updateSimUI();
});
document.getElementById('sim-slower').addEventListener('click', () => {
simSpeed = Math.min(2000, simSpeed + 100);
if (simRunning) { clearInterval(simInterval); simInterval = setInterval(simTick, simSpeed); }
updateSimUI();
});
// Resize waveform panel // Resize waveform panel
const resizeHandle = document.getElementById('wave-resize'); const resizeHandle = document.getElementById('wave-resize');
resizeHandle.addEventListener('mousedown', e => { resizeHandle.addEventListener('mousedown', e => {