refactor: modularize into ES6 modules
Split monolithic index.html into: - js/constants.js - gate config, colors, dimensions - js/state.js - shared application state - js/gates.js - evaluation logic, port geometry - js/renderer.js - canvas drawing - js/waveform.js - GTKWave-style signal viewer - js/simulation.js - clock tick engine - js/events.js - mouse, keyboard, UI handlers - js/app.js - entry point - css/style.css - all styles
This commit is contained in:
193
js/events.js
Normal file
193
js/events.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// 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; });
|
||||
}
|
||||
Reference in New Issue
Block a user