feat: sectioned toolbar + custom component editor

- Redesigned toolbar with I/O, Gates, and Components sections
- Component editor: sub-canvas mode to design reusable chips
  - Save/Cancel with main circuit state preservation
  - Components persist in localStorage
- Custom components render as purple chips with dynamic I/O ports
- Component evaluation simulates internal circuit as black box
- Toolbar height increased to 56px for section labels
- All height references updated consistently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 02:54:04 +01:00
parent 3bff1fd4b4
commit 268013d053
8 changed files with 472 additions and 40 deletions

View File

@@ -1,13 +1,14 @@
// Event handlers — mouse, keyboard, toolbar, waveform controls
import { GATE_W, GATE_H } from './constants.js';
import { GATE_W, GATE_H, COMP_W } from './constants.js';
import { state } from './state.js';
import { evaluateAll, findGateAt, findPortAt } from './gates.js';
import { evaluateAll, findGateAt, findPortAt, getComponentWidth, getComponentHeight } from './gates.js';
import { manualStep, clearWaveData } from './waveform.js';
import { startSim, stopSim, adjustSpeed } from './simulation.js';
import { resize, screenToWorld } from './renderer.js';
import { puzzleMode, currentLevel, showLevelPanel } from './puzzleUI.js';
import { getLevel } from './levels.js';
import { saveState, loadState, exportAsFile, importFromFile } from './saveLoad.js';
import { enterComponentEditor, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
const PAN_SPEED = 40;
@@ -19,6 +20,9 @@ function updateWaveZoomLabel() {
export function initEvents() {
const canvas = document.getElementById('canvas');
// Set up resize callback for component editor
setResizeCallback(resize);
// ==================== CANVAS MOUSE ====================
canvas.addEventListener('mousemove', e => {
state.mouseX = e.offsetX;
@@ -62,13 +66,27 @@ export function initEvents() {
// Placing a new gate
if (state.placingGate) {
state.gates.push({
let w = GATE_W, h = GATE_H;
if (state.placingGate.startsWith('COMPONENT:')) {
const componentId = state.placingGate.substring(10);
const component = state.customComponents?.[componentId];
if (component) {
w = getComponentWidth({ component });
h = getComponentHeight({ component });
}
}
const newGate = {
id: state.nextId++,
type: state.placingGate,
x: world.x - GATE_W / 2,
y: world.y - GATE_H / 2,
x: world.x - w / 2,
y: world.y - h / 2,
value: 0
});
};
if (state.placingGate.startsWith('COMPONENT:')) {
newGate.component = state.customComponents[state.placingGate.substring(10)];
}
state.gates.push(newGate);
evaluateAll();
state.placingGate = null;
return;
@@ -311,4 +329,33 @@ export function initEvents() {
}
});
document.addEventListener('mouseup', () => { state.resizingWave = false; });
// ==================== COMPONENT EDITOR ====================
document.getElementById('create-component-btn').addEventListener('click', () => {
enterComponentEditor();
});
document.getElementById('component-editor-save').addEventListener('click', () => {
const name = prompt('Component name:', 'MyComponent');
if (name && name.trim()) {
exitComponentEditor(name.trim(), true);
}
});
document.getElementById('component-editor-cancel').addEventListener('click', () => {
if (confirm('Discard component without saving?')) {
exitComponentEditor('', false);
}
});
// Event delegation for saved component buttons
document.addEventListener('click', e => {
if (e.target.classList.contains('component-btn')) {
const componentId = e.target.dataset.componentId;
state.placingGate = `COMPONENT:${componentId}`;
}
});
// Update component buttons initially and when customComponents changes
updateComponentButtons();
}