Compare commits

...

2 Commits

Author SHA1 Message Date
Jose Luis
bc8823bcd4 feat: editable labels for INPUT/OUTPUT/CLOCK gates
Double-click any INPUT, OUTPUT, or CLOCK gate to assign a custom label.
Labels are shown inside the gate and used in waveform viewer instead of
generic IN_0/OUT_0 names. Example circuits now ship with meaningful
labels (S, R, D, EN, Q, Q̅, CLK).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 04:06:50 +01:00
Jose Luis
6cb3f091d4 fix: dropdown menus render above component editor overlay
Lower component editor overlay z-index from 105 to 90 so toolbar
dropdown menus (z-index 150) appear on top of it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 04:05:24 +01:00
5 changed files with 47 additions and 22 deletions

View File

@@ -443,7 +443,7 @@ body {
height: 44px;
background: #1a1a2e;
border-bottom: 2px solid #9900ff;
z-index: 105;
z-index: 90;
display: flex;
align-items: center;
padding: 0 12px;

View File

@@ -147,6 +147,20 @@ export function initEvents() {
dragStartPos = null;
});
// Double-click to rename INPUT/OUTPUT/CLOCK gates
canvas.addEventListener('dblclick', e => {
const world = screenToWorld(e.offsetX, e.offsetY);
const gate = findGateAt(world.x, world.y);
if (gate && (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK')) {
const current = gate.label || '';
const label = prompt(`Label for ${gate.type}#${gate.id}:`, current);
if (label !== null) {
gate.label = label.trim() || undefined;
console.log(`[label] ${gate.type}#${gate.id} → "${gate.label || ''}"`);
}
}
});
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
const world = screenToWorld(e.offsetX, e.offsetY);

View File

@@ -20,16 +20,16 @@ const GAP_Y = 100;
function srFlipFlop() {
const gates = [
// Inputs
{ id: 1, type: 'INPUT', x: 50, y: 80, value: 0 }, // S (Set)
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 0 }, // R (Reset)
{ id: 1, type: 'INPUT', x: 50, y: 80, value: 0, label: 'S' },
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 0, label: 'R' },
// Cross-coupled NOR gates
{ id: 3, type: 'NOR', x: 300, y: 80, value: 0 }, // Top NOR → Q
{ id: 4, type: 'NOR', x: 300, y: 280, value: 0 }, // Bottom NOR → Q̅
{ id: 3, type: 'NOR', x: 300, y: 80, value: 0 },
{ id: 4, type: 'NOR', x: 300, y: 280, value: 0 },
// Outputs
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0, label: 'Q' },
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0, label: 'Q̅' }
];
const connections = [
@@ -70,16 +70,16 @@ function srFlipFlop() {
function srFlipFlopNand() {
const gates = [
// Inputs (active low for NAND SR)
{ id: 1, type: 'INPUT', x: 50, y: 80, value: 1 }, // S̅
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 1 }, // R̅
{ id: 1, type: 'INPUT', x: 50, y: 80, value: 1, label: 'S̅' },
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 1, label: 'R̅' },
// Cross-coupled NAND gates
{ id: 3, type: 'NAND', x: 300, y: 80, value: 0 }, // Top NAND → Q
{ id: 4, type: 'NAND', x: 300, y: 280, value: 0 }, // Bottom NAND → Q̅
{ id: 3, type: 'NAND', x: 300, y: 80, value: 0 },
{ id: 4, type: 'NAND', x: 300, y: 280, value: 0 },
// Outputs
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0, label: 'Q' },
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0, label: 'Q̅' }
];
const connections = [
@@ -124,8 +124,8 @@ function srFlipFlopNand() {
function dLatch() {
const gates = [
// Inputs
{ id: 1, type: 'INPUT', x: 50, y: 100, value: 0 }, // D (Data)
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 0 }, // E (Enable)
{ id: 1, type: 'INPUT', x: 50, y: 100, value: 0, label: 'D' },
{ id: 2, type: 'INPUT', x: 50, y: 280, value: 0, label: 'EN' },
// NOT gate to invert D
{ id: 3, type: 'NOT', x: 200, y: 340, value: 0 },
@@ -139,8 +139,8 @@ function dLatch() {
{ id: 7, type: 'NOR', x: 550, y: 340, value: 0 }, // → Q̅
// Outputs
{ id: 8, type: 'OUTPUT', x: 750, y: 60, value: 0 }, // Q
{ id: 9, type: 'OUTPUT', x: 750, y: 340, value: 0 } // Q̅
{ id: 8, type: 'OUTPUT', x: 750, y: 60, value: 0, label: 'Q' },
{ id: 9, type: 'OUTPUT', x: 750, y: 340, value: 0, label: 'Q̅' }
];
const connections = [
@@ -189,8 +189,8 @@ function dLatch() {
function dFlipFlop() {
const gates = [
// Inputs
{ id: 1, type: 'INPUT', x: 30, y: 100, value: 0 }, // D
{ id: 2, type: 'CLOCK', x: 30, y: 350, value: 0 }, // CLK
{ id: 1, type: 'INPUT', x: 30, y: 100, value: 0, label: 'D' },
{ id: 2, type: 'CLOCK', x: 30, y: 350, value: 0, label: 'CLK' },
// CLK inverter (for master latch)
{ id: 3, type: 'NOT', x: 170, y: 350, value: 0 },
@@ -210,8 +210,8 @@ function dFlipFlop() {
{ id: 13, type: 'NOR', x: 830, y: 240, value: 0 }, // Slave Q̅
// Outputs
{ id: 14, type: 'OUTPUT', x: 1010, y: 60, value: 0 }, // Q
{ id: 15, type: 'OUTPUT', x: 1010, y: 240, value: 0 } // Q̅
{ id: 14, type: 'OUTPUT', x: 1010, y: 60, value: 0, label: 'Q' },
{ id: 15, type: 'OUTPUT', x: 1010, y: 240, value: 0, label: 'Q̅' }
];
const connections = [

View File

@@ -63,8 +63,18 @@ function drawGate(gate) {
ctx.textBaseline = 'middle';
const isIOType = gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK';
// Show custom label above the gate if it has one
if (gate.label && isIOType) {
ctx.font = 'bold 10px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#888';
ctx.fillText(gate.label, gate.x + GATE_W / 2, gate.y - 8);
}
ctx.font = `bold 14px "Segoe UI", system-ui, sans-serif`;
ctx.fillStyle = isActive ? '#fff' : color;
ctx.fillText(
gate.type === 'CLOCK' ? '⏱ CLK' : gate.type,
gate.label && isIOType ? gate.label : (gate.type === 'CLOCK' ? '⏱ CLK' : gate.type),
gate.x + GATE_W / 2,
gate.y + GATE_H / 2 - (isIOType ? 8 : 0)
);

View File

@@ -12,6 +12,7 @@ export function getTrackedGates() {
}
export function getGateLabel(gate) {
if (gate.label) return gate.label;
const sameType = state.gates.filter(g => g.type === gate.type);
const idx = sameType.indexOf(gate);
if (gate.type === 'CLOCK') return `CLK_${idx}`;