// Event handlers — mouse, keyboard, toolbar, waveform controls import { GATE_W, GATE_H } from './constants.js'; import { state } from './state.js'; import { evaluateAll, findGateAt, findPortAt } from './gates.js'; import { manualStep, clearWaveData, updateWaveInfo } from './waveform.js'; import { startSim, stopSim, adjustSpeed } from './simulation.js'; import { resize } from './renderer.js'; export function initEvents() { const canvas = document.getElementById('canvas'); // ==================== CANVAS MOUSE ==================== canvas.addEventListener('mousemove', e => { state.mouseX = e.offsetX; state.mouseY = e.offsetY; state.hoveredPort = findPortAt(state.mouseX, state.mouseY); state.hoveredGate = state.hoveredPort ? state.hoveredPort.gate : findGateAt(state.mouseX, state.mouseY); if (state.dragging) { state.dragging.x = state.mouseX - state.dragOffset.x; state.dragging.y = state.mouseY - state.dragOffset.y; evaluateAll(); } canvas.style.cursor = state.placingGate ? 'crosshair' : state.hoveredPort ? 'pointer' : state.hoveredGate ? 'grab' : 'default'; }); canvas.addEventListener('mousedown', e => { if (e.button !== 0) return; const x = e.offsetX, y = e.offsetY; // Placing a new gate if (state.placingGate) { state.gates.push({ id: state.nextId++, type: state.placingGate, x: x - GATE_W / 2, y: y - GATE_H / 2, value: 0 }); evaluateAll(); state.placingGate = null; return; } // Port click — connecting const port = findPortAt(x, y); if (port) { if (state.connecting) { if (state.connecting.portType === 'output' && port.type === 'input') { state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index)); state.connections.push({ from: state.connecting.gate.id, fromPort: state.connecting.portIndex, to: port.gate.id, toPort: port.index }); evaluateAll(); } else if (state.connecting.portType === 'input' && port.type === 'output') { state.connections = state.connections.filter(c => !(c.to === state.connecting.gate.id && c.toPort === state.connecting.portIndex)); state.connections.push({ from: port.gate.id, fromPort: port.index, to: state.connecting.gate.id, toPort: state.connecting.portIndex }); evaluateAll(); } state.connecting = null; } else { state.connecting = { gate: port.gate, portIndex: port.index, portType: port.type }; } return; } if (state.connecting) { state.connecting = null; return; } // Toggle INPUT/CLOCK const gate = findGateAt(x, y); if (gate && (gate.type === 'INPUT' || gate.type === 'CLOCK')) { gate.value = gate.value ? 0 : 1; evaluateAll(); return; } // Drag gate if (gate) { state.dragging = gate; state.dragOffset = { x: x - gate.x, y: y - gate.y }; canvas.style.cursor = 'grabbing'; } }); canvas.addEventListener('mouseup', () => { state.dragging = null; }); canvas.addEventListener('contextmenu', e => { e.preventDefault(); const x = e.offsetX, y = e.offsetY; const port = findPortAt(x, y); if (port && port.type === 'input') { state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index)); evaluateAll(); } else if (port && port.type === 'output') { state.connections = state.connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index)); evaluateAll(); } }); // ==================== KEYBOARD ==================== document.addEventListener('keydown', e => { if (e.key === 'Delete' || e.key === 'Backspace') { if (state.hoveredGate && document.activeElement === document.body) { e.preventDefault(); const gateId = state.hoveredGate.id; state.connections = state.connections.filter(c => c.from !== gateId && c.to !== gateId); state.gates = state.gates.filter(g => g.id !== gateId); delete state.waveData[gateId]; state.hoveredGate = null; evaluateAll(); } } if (e.key === 'Escape') { state.placingGate = null; state.connecting = null; } }); // ==================== TOOLBAR ==================== document.querySelectorAll('.gate-btn').forEach(btn => { btn.addEventListener('click', () => { state.placingGate = btn.dataset.gate; }); }); document.getElementById('clear-btn').addEventListener('click', () => { if (state.gates.length === 0 || confirm('Clear all gates and connections?')) { state.gates = []; state.connections = []; state.connecting = null; state.placingGate = null; clearWaveData(); } }); // Help modal document.getElementById('help-btn').addEventListener('click', () => { document.getElementById('help-modal').classList.add('visible'); }); document.getElementById('help-close').addEventListener('click', () => { document.getElementById('help-modal').classList.remove('visible'); }); document.getElementById('help-modal').addEventListener('click', e => { if (e.target === e.currentTarget) document.getElementById('help-modal').classList.remove('visible'); }); // Waveform toggle document.getElementById('sim-btn').addEventListener('click', () => { state.waveformVisible = !state.waveformVisible; document.getElementById('waveform-panel').classList.toggle('visible', state.waveformVisible); document.getElementById('sim-btn').classList.toggle('active', state.waveformVisible); resize(); }); // ==================== WAVEFORM CONTROLS ==================== document.getElementById('wave-record').addEventListener('click', function() { state.recording = !state.recording; this.classList.toggle('active', state.recording); this.textContent = state.recording ? '⏺ Record' : '⏸ Paused'; }); document.getElementById('wave-clear').addEventListener('click', clearWaveData); document.getElementById('wave-step').addEventListener('click', manualStep); document.getElementById('wave-zoom-in').addEventListener('click', () => { state.waveZoom = Math.min(60, state.waveZoom + 5); }); document.getElementById('wave-zoom-out').addEventListener('click', () => { state.waveZoom = Math.max(5, state.waveZoom - 5); }); // ==================== SIMULATION CONTROLS ==================== document.getElementById('sim-run-btn').addEventListener('click', () => { if (state.simRunning) stopSim(); else startSim(); }); document.getElementById('sim-faster').addEventListener('click', () => adjustSpeed(-100)); document.getElementById('sim-slower').addEventListener('click', () => adjustSpeed(100)); // ==================== WAVEFORM PANEL RESIZE ==================== const resizeHandle = document.getElementById('wave-resize'); resizeHandle.addEventListener('mousedown', e => { state.resizingWave = true; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (state.resizingWave) { state.waveformHeight = Math.max(100, Math.min(500, window.innerHeight - e.clientY)); document.getElementById('waveform-panel').style.height = state.waveformHeight + 'px'; resize(); } }); document.addEventListener('mouseup', () => { state.resizingWave = false; }); }