// 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 } from './waveform.js'; import { startSim, stopSim, adjustSpeed } from './simulation.js'; import { resize, screenToWorld } from './renderer.js'; const PAN_SPEED = 40; export function initEvents() { const canvas = document.getElementById('canvas'); // ==================== CANVAS MOUSE ==================== canvas.addEventListener('mousemove', e => { state.mouseX = e.offsetX; state.mouseY = e.offsetY; // Convert to world coords for gate/port detection const world = screenToWorld(e.offsetX, e.offsetY); state.hoveredPort = findPortAt(world.x, world.y); state.hoveredGate = state.hoveredPort ? state.hoveredPort.gate : findGateAt(world.x, world.y); if (state.dragging) { state.dragging.x = world.x - state.dragOffset.x; state.dragging.y = world.y - 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 world = screenToWorld(e.offsetX, e.offsetY); // Placing a new gate if (state.placingGate) { state.gates.push({ id: state.nextId++, type: state.placingGate, x: world.x - GATE_W / 2, y: world.y - GATE_H / 2, value: 0 }); evaluateAll(); state.placingGate = null; return; } // Port click — connecting const port = findPortAt(world.x, world.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(world.x, world.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: world.x - gate.x, y: world.y - gate.y }; canvas.style.cursor = 'grabbing'; } }); canvas.addEventListener('mouseup', () => { state.dragging = null; }); canvas.addEventListener('contextmenu', e => { e.preventDefault(); const world = screenToWorld(e.offsetX, e.offsetY); const port = findPortAt(world.x, world.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(); } }); // ==================== MOUSE WHEEL ZOOM ==================== canvas.addEventListener('wheel', e => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; const newZoom = Math.max(0.2, Math.min(3, state.zoom + delta)); // Zoom towards mouse position const worldBefore = screenToWorld(e.offsetX, e.offsetY); state.zoom = newZoom; const worldAfter = screenToWorld(e.offsetX, e.offsetY); state.camX += (worldAfter.x - worldBefore.x) * state.zoom; state.camY += (worldAfter.y - worldBefore.y) * state.zoom; }, { passive: false }); // ==================== KEYBOARD ==================== const keysDown = new Set(); document.addEventListener('keydown', e => { keysDown.add(e.key); 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; } // Pan with arrow keys if (e.key === 'ArrowLeft') { state.camX += PAN_SPEED; e.preventDefault(); } if (e.key === 'ArrowRight') { state.camX -= PAN_SPEED; e.preventDefault(); } if (e.key === 'ArrowUp') { state.camY += PAN_SPEED; e.preventDefault(); } if (e.key === 'ArrowDown') { state.camY -= PAN_SPEED; e.preventDefault(); } // Zoom with +/- keys if ((e.key === '+' || e.key === '=') && document.activeElement === document.body) { state.zoom = Math.min(3, state.zoom + 0.1); e.preventDefault(); } if ((e.key === '-' || e.key === '_') && document.activeElement === document.body) { state.zoom = Math.max(0.2, state.zoom - 0.1); e.preventDefault(); } // Reset view with 0 if (e.key === '0' && document.activeElement === document.body) { state.camX = 0; state.camY = 0; state.zoom = 1; } }); document.addEventListener('keyup', e => { keysDown.delete(e.key); }); // ==================== 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; }); }