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.input-btn { border-color: #3388ff; }
.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:hover { border-color: #ffaa55; }
.separator { width: 1px; height: 24px; background: #2a2a3a; margin: 0 6px; }
@@ -229,6 +231,7 @@
<div id="toolbar">
<span class="logo">⚡ Logic Lab</span>
<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>
<div class="separator"></div>
<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-zoom-in">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>
</div>
<div id="wave-container">
@@ -276,6 +284,13 @@
<li>Right-click a port to delete its connections</li>
<li><kbd>Escape</kbd> to cancel placing/connecting</li>
</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>
<ul>
<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>Toggle inputs to see signals change in real-time</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>
<p style="margin-top: 12px; color: #666;">Built with ❤️ at MontLab</p>
<button id="help-close">Got it</button>
@@ -312,7 +327,7 @@
const GATE_COLORS = {
AND: '#00e599', OR: '#3388ff', NOT: '#ff6644',
NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa',
INPUT: '#3388ff', OUTPUT: '#ff8833'
INPUT: '#3388ff', CLOCK: '#ff44aa', OUTPUT: '#ff8833'
};
const SIGNAL_COLORS = [
@@ -331,8 +346,14 @@
let waveScroll = 0;
let resizingWave = false;
// Clock / simulation state
let simRunning = false;
let simInterval = null;
let simSpeed = 500; // ms per tick
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;
}
function gateOutputCount(type) {
@@ -342,7 +363,7 @@
function evaluate(gate, visited = new Set()) {
if (visited.has(gate.id)) return gate.value || 0;
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 inputs = [];
@@ -371,7 +392,7 @@
}
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));
if (recording && waveformVisible) recordSample();
}
@@ -413,16 +434,17 @@
}
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 outputs = gates.filter(g => g.type === 'OUTPUT');
const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT');
return [...inputs, ...logic, ...outputs];
const logic = gates.filter(g => g.type !== 'INPUT' && g.type !== 'OUTPUT' && g.type !== 'CLOCK');
return [...clocks, ...inputs, ...logic, ...outputs];
}
function getGateLabel(gate) {
const sameType = 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}`;
@@ -633,15 +655,16 @@
ctx.textBaseline = 'middle';
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
ctx.font = '9px monospace';
ctx.fillStyle = '#444';
ctx.fillText(label, gate.x + GATE_W / 2, gate.y + GATE_H - 6);
// Value for INPUT/OUTPUT
if (gate.type === 'INPUT' || gate.type === 'OUTPUT') {
// Value for INPUT/OUTPUT/CLOCK
if (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK') {
ctx.font = 'bold 16px monospace';
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
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; }
const gate = findGateAt(x, y);
if (gate && gate.type === 'INPUT') {
if (gate && (gate.type === 'INPUT' || gate.type === 'CLOCK')) {
gate.value = gate.value ? 0 : 1;
evaluateAll();
return;
@@ -924,6 +947,77 @@
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
const resizeHandle = document.getElementById('wave-resize');
resizeHandle.addEventListener('mousedown', e => {