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:
101
js/renderer.js
101
js/renderer.js
@@ -1,7 +1,7 @@
|
||||
// Canvas rendering — gates, connections, grid
|
||||
import { GATE_W, GATE_H, PORT_R, GATE_COLORS } from './constants.js';
|
||||
import { GATE_W, GATE_H, COMP_W, PORT_R, GATE_COLORS } from './constants.js';
|
||||
import { state } from './state.js';
|
||||
import { getInputPorts, getOutputPorts } from './gates.js';
|
||||
import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js';
|
||||
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
||||
|
||||
let canvas, ctx;
|
||||
@@ -19,7 +19,8 @@ export function resize() {
|
||||
const sidebarW = sidebarOpen ? 340 : 0;
|
||||
canvas.width = window.innerWidth - sidebarW;
|
||||
const waveH = state.waveformVisible ? state.waveformHeight : 0;
|
||||
canvas.height = window.innerHeight - 48 - waveH;
|
||||
const editorH = state.componentEditorActive ? 44 : 0;
|
||||
canvas.height = window.innerHeight - 56 - editorH - waveH;
|
||||
}
|
||||
|
||||
// Convert screen coords to world coords (accounting for pan/zoom)
|
||||
@@ -31,6 +32,11 @@ export function screenToWorld(sx, sy) {
|
||||
}
|
||||
|
||||
function drawGate(gate) {
|
||||
// Component gates have different rendering
|
||||
if (gate.type.startsWith('COMPONENT:')) {
|
||||
return drawComponentGate(gate);
|
||||
}
|
||||
|
||||
const color = GATE_COLORS[gate.type];
|
||||
const isHovered = state.hoveredGate === gate;
|
||||
const isActive = gate.value === 1;
|
||||
@@ -110,6 +116,76 @@ function drawGate(gate) {
|
||||
});
|
||||
}
|
||||
|
||||
function drawComponentGate(gate) {
|
||||
const isHovered = state.hoveredGate === gate;
|
||||
const isActive = gate.value === 1;
|
||||
const w = getComponentWidth(gate);
|
||||
const h = getComponentHeight(gate);
|
||||
const color = '#9900ff';
|
||||
|
||||
if (isActive) {
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 20 * state.zoom;
|
||||
}
|
||||
|
||||
ctx.fillStyle = isActive ? color + '22' : '#14141e';
|
||||
ctx.strokeStyle = isHovered ? '#fff' : color;
|
||||
ctx.lineWidth = (isHovered ? 2.5 : 1.5);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(gate.x, gate.y, w, h, 8);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Component name label
|
||||
ctx.fillStyle = isActive ? '#fff' : color;
|
||||
ctx.font = `bold 12px "Segoe UI", system-ui, sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const componentName = gate.component?.name || 'Component';
|
||||
ctx.fillText(componentName, gate.x + w / 2, gate.y + h / 2);
|
||||
|
||||
// Small ID label
|
||||
ctx.font = '9px monospace';
|
||||
ctx.fillStyle = '#444';
|
||||
ctx.fillText(getGateLabel(gate), gate.x + w / 2, gate.y + h - 6);
|
||||
|
||||
// Input ports
|
||||
getInputPorts(gate).forEach(p => {
|
||||
const isPortHovered = state.hoveredPort &&
|
||||
state.hoveredPort.gate === gate &&
|
||||
state.hoveredPort.index === p.index &&
|
||||
state.hoveredPort.type === 'input';
|
||||
const conn = state.connections.find(c => c.to === gate.id && c.toPort === p.index);
|
||||
const portActive = conn ? state.gates.find(g => g.id === conn.from)?.value : 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Output ports
|
||||
getOutputPorts(gate).forEach(p => {
|
||||
const isPortHovered = state.hoveredPort &&
|
||||
state.hoveredPort.gate === gate &&
|
||||
state.hoveredPort.index === p.index &&
|
||||
state.hoveredPort.type === 'output';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
function drawConnection(conn) {
|
||||
const fromGate = state.gates.find(g => g.id === conn.from);
|
||||
const toGate = state.gates.find(g => g.id === conn.to);
|
||||
@@ -181,12 +257,25 @@ function drawPlacingGhost() {
|
||||
if (!state.placingGate) return;
|
||||
ctx.globalAlpha = 0.5;
|
||||
const world = screenToWorld(state.mouseX, state.mouseY);
|
||||
|
||||
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) {
|
||||
const count = Math.max(component.inputCount, component.outputCount);
|
||||
w = 120;
|
||||
h = Math.max(60, (count + 1) * 25);
|
||||
}
|
||||
}
|
||||
|
||||
const ghost = {
|
||||
x: world.x - GATE_W / 2,
|
||||
y: world.y - GATE_H / 2,
|
||||
x: world.x - w / 2,
|
||||
y: world.y - h / 2,
|
||||
type: state.placingGate,
|
||||
value: 0,
|
||||
id: -1
|
||||
id: -1,
|
||||
component: state.customComponents?.[state.placingGate.substring(10)]
|
||||
};
|
||||
drawGate(ghost);
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
Reference in New Issue
Block a user