feat: add Examples dropdown with pre-built circuits

Add 4 example circuits accessible from a new toolbar dropdown:
- SR Flip-Flop (NOR) — basic set-reset latch
- SR Flip-Flop (NAND) — active-low variant
- D Latch (1-bit Memory) — gated latch with enable
- D Flip-Flop (Master-Slave) — edge-triggered with CLK

Each example shows name + description in the dropdown and loads
the full circuit with proper gate placement and wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 03:54:04 +01:00
parent d78b45841c
commit 2384c489b9
4 changed files with 376 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ 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';
import { getExampleList, loadExample } from './examples.js';
const PAN_SPEED = 40;
@@ -255,6 +256,34 @@ export function initEvents() {
});
});
// ==================== EXAMPLES ====================
const examplesMenu = document.getElementById('examples-menu');
getExampleList().forEach((ex, i) => {
const btn = document.createElement('button');
btn.className = 'example-btn';
btn.innerHTML = `<span class="example-name">${ex.name}</span><span class="example-desc">${ex.description}</span>`;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const hasGates = state.gates.length > 0;
if (hasGates && !confirm('Load example? This will replace your current circuit.')) return;
const data = loadExample(i);
if (data) {
state.gates = data.circuit.gates;
state.connections = data.circuit.connections;
state.nextId = data.circuit.nextId;
if (data.camera) {
state.camX = data.camera.camX;
state.camY = data.camera.camY;
state.zoom = data.camera.zoom;
}
clearWaveData();
evaluateAll();
}
closeAllDropdowns();
});
examplesMenu.appendChild(btn);
});
document.getElementById('clear-btn').addEventListener('click', () => {
if (state.gates.length === 0 || confirm('Clear all gates and connections?')) {
state.gates = [];

300
js/examples.js Normal file
View File

@@ -0,0 +1,300 @@
// Pre-built example circuits
// Each example is a { gates, connections, nextId, camera } object
const W = 100; // GATE_W
const H = 60; // GATE_H
const GAP_X = 160;
const GAP_Y = 100;
/**
* SR Flip-Flop using cross-coupled NOR gates
*
* S ──┐
* NOR─── Q
* ┌──┘ │
* │ │
* └──┐ │
* NOR─── Q̅
* R ──┘
*/
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)
// 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̅
// Outputs
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅
];
const connections = [
// S → Top NOR input 0
{ from: 1, fromPort: 0, to: 3, toPort: 0 },
// R → Bottom NOR input 1
{ from: 2, fromPort: 0, to: 4, toPort: 1 },
// Top NOR (Q) → Bottom NOR input 0 (cross-couple)
{ from: 3, fromPort: 0, to: 4, toPort: 0 },
// Bottom NOR (Q̅) → Top NOR input 1 (cross-couple)
{ from: 4, fromPort: 0, to: 3, toPort: 1 },
// NOR outputs → visible outputs
{ from: 3, fromPort: 0, to: 5, toPort: 0 },
{ from: 4, fromPort: 0, to: 6, toPort: 0 }
];
return {
name: 'SR Flip-Flop (NOR)',
description: 'Set-Reset latch using cross-coupled NOR gates. Toggle S to set Q=1, toggle R to reset Q=0.',
gates,
connections,
nextId: 7,
camera: { camX: 0, camY: 0, zoom: 1 }
};
}
/**
* SR Flip-Flop using cross-coupled NAND gates
*
* S̅ ──┐
* NAND── Q
* ┌───┘ │
* │ │
* └───┐ │
* NAND── Q̅
* R̅ ──┘
*/
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̅
// 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̅
// Outputs
{ id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q
{ id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅
];
const connections = [
// S̅ → Top NAND input 0
{ from: 1, fromPort: 0, to: 3, toPort: 0 },
// R̅ → Bottom NAND input 1
{ from: 2, fromPort: 0, to: 4, toPort: 1 },
// Top NAND (Q) → Bottom NAND input 0 (cross-couple)
{ from: 3, fromPort: 0, to: 4, toPort: 0 },
// Bottom NAND (Q̅) → Top NAND input 1 (cross-couple)
{ from: 4, fromPort: 0, to: 3, toPort: 1 },
// Outputs
{ from: 3, fromPort: 0, to: 5, toPort: 0 },
{ from: 4, fromPort: 0, to: 6, toPort: 0 }
];
return {
name: 'SR Flip-Flop (NAND)',
description: 'Set-Reset latch using cross-coupled NAND gates. Inputs are active-low: set S̅=0 to set, R̅=0 to reset.',
gates,
connections,
nextId: 7,
camera: { camX: 0, camY: 0, zoom: 1 }
};
}
/**
* Gated D Latch (1-bit memory)
*
* Uses an SR flip-flop with gating logic:
* D ──AND──┐
* E ──┤ NOR── Q
* │ │ ┌──┘│
* │ │ │ │
* │ │ └──┐│
* E ──┤ NOR── Q̅
* D─NOT─AND┘
*
* When Enable=1, Q follows D.
* When Enable=0, Q holds its value.
*/
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)
// NOT gate to invert D
{ id: 3, type: 'NOT', x: 200, y: 340, value: 0 },
// AND gates for gating
{ id: 4, type: 'AND', x: 350, y: 60, value: 0 }, // D AND E → S
{ id: 5, type: 'AND', x: 350, y: 340, value: 0 }, // NOT(D) AND E → R
// Cross-coupled NOR gates (SR latch core)
{ id: 6, type: 'NOR', x: 550, y: 60, value: 0 }, // → Q
{ 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̅
];
const connections = [
// D → AND top input, and D → NOT
{ from: 1, fromPort: 0, to: 4, toPort: 0 },
{ from: 1, fromPort: 0, to: 3, toPort: 0 },
// E → both AND gates
{ from: 2, fromPort: 0, to: 4, toPort: 1 },
{ from: 2, fromPort: 0, to: 5, toPort: 1 },
// NOT(D) → bottom AND
{ from: 3, fromPort: 0, to: 5, toPort: 0 },
// AND outputs → NOR inputs (S and R)
{ from: 4, fromPort: 0, to: 6, toPort: 0 },
{ from: 5, fromPort: 0, to: 7, toPort: 1 },
// Cross-coupling
{ from: 6, fromPort: 0, to: 7, toPort: 0 },
{ from: 7, fromPort: 0, to: 6, toPort: 1 },
// Outputs
{ from: 6, fromPort: 0, to: 8, toPort: 0 },
{ from: 7, fromPort: 0, to: 9, toPort: 0 }
];
return {
name: 'D Latch (1-bit Memory)',
description: 'Gated D latch — a 1-bit memory cell. When Enable=1, output Q follows input D. When Enable=0, Q holds its last value.',
gates,
connections,
nextId: 10,
camera: { camX: 0, camY: 0, zoom: 1 }
};
}
/**
* D Flip-Flop (edge-triggered, master-slave)
*
* Two D latches in series with inverted enable:
* - Master latch captures D when CLK=0
* - Slave latch outputs when CLK=1
* This creates rising-edge triggered behavior.
*/
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
// CLK inverter (for master latch)
{ id: 3, type: 'NOT', x: 170, y: 350, value: 0 },
// === MASTER LATCH (enabled when CLK=0, i.e. NOT CLK=1) ===
{ id: 4, type: 'NOT', x: 170, y: 200, value: 0 }, // NOT D for master
{ id: 5, type: 'AND', x: 300, y: 60, value: 0 }, // D AND !CLK
{ id: 6, type: 'AND', x: 300, y: 240, value: 0 }, // !D AND !CLK
{ id: 7, type: 'NOR', x: 450, y: 60, value: 0 }, // Master Q
{ id: 8, type: 'NOR', x: 450, y: 240, value: 0 }, // Master Q̅
// === SLAVE LATCH (enabled when CLK=1) ===
{ id: 9, type: 'NOT', x: 570, y: 200, value: 0 }, // NOT master Q for slave
{ id: 10, type: 'AND', x: 680, y: 60, value: 0 }, // masterQ AND CLK
{ id: 11, type: 'AND', x: 680, y: 240, value: 0 }, // !masterQ AND CLK
{ id: 12, type: 'NOR', x: 830, y: 60, value: 0 }, // Slave Q
{ 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̅
];
const connections = [
// D → master AND, D → master NOT
{ from: 1, fromPort: 0, to: 5, toPort: 0 },
{ from: 1, fromPort: 0, to: 4, toPort: 0 },
// CLK → NOT (invert for master)
{ from: 2, fromPort: 0, to: 3, toPort: 0 },
// !CLK → master AND gates (enable)
{ from: 3, fromPort: 0, to: 5, toPort: 1 },
{ from: 3, fromPort: 0, to: 6, toPort: 1 },
// !D → master bottom AND
{ from: 4, fromPort: 0, to: 6, toPort: 0 },
// Master AND outputs → Master NOR (SR latch)
{ from: 5, fromPort: 0, to: 7, toPort: 0 },
{ from: 6, fromPort: 0, to: 8, toPort: 1 },
// Master cross-coupling
{ from: 7, fromPort: 0, to: 8, toPort: 0 },
{ from: 8, fromPort: 0, to: 7, toPort: 1 },
// Master Q → slave AND, Master Q → slave NOT
{ from: 7, fromPort: 0, to: 10, toPort: 0 },
{ from: 7, fromPort: 0, to: 9, toPort: 0 },
// CLK → slave AND gates (enable, direct CLK)
{ from: 2, fromPort: 0, to: 10, toPort: 1 },
{ from: 2, fromPort: 0, to: 11, toPort: 1 },
// !masterQ → slave bottom AND
{ from: 9, fromPort: 0, to: 11, toPort: 0 },
// Slave AND outputs → Slave NOR (SR latch)
{ from: 10, fromPort: 0, to: 12, toPort: 0 },
{ from: 11, fromPort: 0, to: 13, toPort: 1 },
// Slave cross-coupling
{ from: 12, fromPort: 0, to: 13, toPort: 0 },
{ from: 13, fromPort: 0, to: 12, toPort: 1 },
// Outputs
{ from: 12, fromPort: 0, to: 14, toPort: 0 },
{ from: 13, fromPort: 0, to: 15, toPort: 0 }
];
return {
name: 'D Flip-Flop (Master-Slave)',
description: 'Edge-triggered D flip-flop built from two D latches. Captures D on the rising edge of CLK. Use with the clock simulation to see it in action.',
gates,
connections,
nextId: 16,
camera: { camX: 0, camY: -50, zoom: 0.9 }
};
}
// Export all examples as a list
export const examples = [
srFlipFlop,
srFlipFlopNand,
dLatch,
dFlipFlop
];
export function getExampleList() {
return examples.map((fn, i) => {
const ex = fn();
return { id: i, name: ex.name, description: ex.description };
});
}
export function loadExample(index) {
if (index < 0 || index >= examples.length) return null;
const ex = examples[index]();
return {
circuit: {
gates: JSON.parse(JSON.stringify(ex.gates)),
connections: JSON.parse(JSON.stringify(ex.connections)),
nextId: ex.nextId
},
camera: ex.camera
};
}