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>
This commit is contained in:
Jose Luis
2026-03-20 04:06:50 +01:00
parent 6cb3f091d4
commit bc8823bcd4
4 changed files with 46 additions and 21 deletions

View File

@@ -147,6 +147,20 @@ export function initEvents() {
dragStartPos = null; 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 => { canvas.addEventListener('contextmenu', e => {
e.preventDefault(); e.preventDefault();
const world = screenToWorld(e.offsetX, e.offsetY); const world = screenToWorld(e.offsetX, e.offsetY);

View File

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

View File

@@ -63,8 +63,18 @@ function drawGate(gate) {
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
const isIOType = gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK'; 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( 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.x + GATE_W / 2,
gate.y + GATE_H / 2 - (isIOType ? 8 : 0) gate.y + GATE_H / 2 - (isIOType ? 8 : 0)
); );

View File

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