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:
77
js/gates.js
77
js/gates.js
@@ -1,13 +1,52 @@
|
||||
// Gate evaluation and port geometry
|
||||
import { GATE_W, GATE_H, PORT_R, gateInputCount, gateOutputCount } from './constants.js';
|
||||
import { GATE_W, GATE_H, COMP_W, PORT_R, gateInputCount as baseGateInputCount, gateOutputCount as baseGateOutputCount } from './constants.js';
|
||||
import { state } from './state.js';
|
||||
import { recordSample, setEvaluateAll } from './waveform.js';
|
||||
import { evaluateComponent } from './components.js';
|
||||
|
||||
// Wrappers that handle component types
|
||||
export function gateInputCount(type) {
|
||||
if (type.startsWith('COMPONENT:')) {
|
||||
const componentId = type.substring(10);
|
||||
const component = state.customComponents?.[componentId];
|
||||
return component ? component.inputCount : 0;
|
||||
}
|
||||
return baseGateInputCount(type);
|
||||
}
|
||||
|
||||
export function gateOutputCount(type) {
|
||||
if (type.startsWith('COMPONENT:')) {
|
||||
const componentId = type.substring(10);
|
||||
const component = state.customComponents?.[componentId];
|
||||
return component ? component.outputCount : 0;
|
||||
}
|
||||
return baseGateOutputCount(type);
|
||||
}
|
||||
|
||||
export function getComponentWidth(gate) {
|
||||
if (gate.type.startsWith('COMPONENT:')) {
|
||||
const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1);
|
||||
return Math.max(120, (count + 1) * 25);
|
||||
}
|
||||
return GATE_W;
|
||||
}
|
||||
|
||||
export function getComponentHeight(gate) {
|
||||
if (gate.type.startsWith('COMPONENT:')) {
|
||||
const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1);
|
||||
return Math.max(60, (count + 1) * 25);
|
||||
}
|
||||
return GATE_H;
|
||||
}
|
||||
|
||||
export function getInputPorts(gate) {
|
||||
const count = gateInputCount(gate.type);
|
||||
const ports = [];
|
||||
const isComponent = gate.type.startsWith('COMPONENT:');
|
||||
const gateHeight = isComponent ? getComponentHeight(gate) : GATE_H;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const spacing = GATE_H / (count + 1);
|
||||
const spacing = gateHeight / (count + 1);
|
||||
ports.push({ x: gate.x, y: gate.y + spacing * (i + 1), index: i, type: 'input' });
|
||||
}
|
||||
return ports;
|
||||
@@ -16,8 +55,13 @@ export function getInputPorts(gate) {
|
||||
export function getOutputPorts(gate) {
|
||||
const count = gateOutputCount(gate.type);
|
||||
const ports = [];
|
||||
const isComponent = gate.type.startsWith('COMPONENT:');
|
||||
const gateWidth = isComponent ? getComponentWidth(gate) : GATE_W;
|
||||
const gateHeight = isComponent ? getComponentHeight(gate) : GATE_H;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
ports.push({ x: gate.x + GATE_W, y: gate.y + GATE_H / 2, index: i, type: 'output' });
|
||||
const spacing = gateHeight / (count + 1);
|
||||
ports.push({ x: gate.x + gateWidth, y: gate.y + spacing * (i + 1), index: i, type: 'output' });
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
@@ -40,14 +84,19 @@ export function evaluate(gate, visited = new Set()) {
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
switch (gate.type) {
|
||||
case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break;
|
||||
case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break;
|
||||
case 'NOT': result = inputs[0] ? 0 : 1; break;
|
||||
case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break;
|
||||
case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break;
|
||||
case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break;
|
||||
case 'OUTPUT': result = inputs[0] || 0; break;
|
||||
if (gate.type.startsWith('COMPONENT:')) {
|
||||
const outputs = evaluateComponent(gate, inputs);
|
||||
result = outputs[0] || 0;
|
||||
} else {
|
||||
switch (gate.type) {
|
||||
case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break;
|
||||
case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break;
|
||||
case 'NOT': result = inputs[0] ? 0 : 1; break;
|
||||
case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break;
|
||||
case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break;
|
||||
case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break;
|
||||
case 'OUTPUT': result = inputs[0] || 0; break;
|
||||
}
|
||||
}
|
||||
gate.value = result;
|
||||
return result;
|
||||
@@ -65,7 +114,11 @@ export function evaluateAll(recordWave = false) {
|
||||
setEvaluateAll(evaluateAll);
|
||||
|
||||
export function findGateAt(x, y) {
|
||||
return state.gates.find(g => x >= g.x && x <= g.x + GATE_W && y >= g.y && y <= g.y + GATE_H);
|
||||
return state.gates.find(g => {
|
||||
const w = g.type.startsWith('COMPONENT:') ? getComponentWidth(g) : GATE_W;
|
||||
const h = g.type.startsWith('COMPONENT:') ? getComponentHeight(g) : GATE_H;
|
||||
return x >= g.x && x <= g.x + w && y >= g.y && y <= g.y + h;
|
||||
});
|
||||
}
|
||||
|
||||
export function findPortAt(x, y) {
|
||||
|
||||
Reference in New Issue
Block a user