Refactor code structure for improved readability and maintainability

This commit is contained in:
David Montero Crespo 2026-04-21 12:59:59 -03:00
parent 9ce4ad0147
commit eaf3fffd36
10 changed files with 1147 additions and 27 deletions

View File

@ -0,0 +1,53 @@
/**
* Live ngspice check for a handful of representative analog examples.
*
* We don't run every one of the 30 circuits through WASM ngspice here —
* that'd be ~15 s of CI time. Instead, four archetypes cover the main
* element classes:
* - an-voltage-divider (resistors + DC source)
* - an-half-wave-rectifier (diode + AC source, .tran)
* - an-bjt-switch (BJT + switch waveform)
* - an-opamp-follower (LM358 behavioral op-amp)
*
* Anything else should be covered by the per-mapper tests in
* component-to-spice.test.ts + the shape tests in examples-analog.test.ts.
*/
import { describe, it, expect } from 'vitest';
import { analogExamples } from '../data/examples-analog';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
import { runNetlist } from '../simulation/spice/SpiceEngine';
const ARCHETYPES = [
'an-voltage-divider',
'an-half-wave-rectifier',
'an-bjt-switch',
'an-opamp-follower',
];
describe('analogExamples — representative ngspice solves', () => {
for (const id of ARCHETYPES) {
it(`${id} is solvable by ngspice`, { timeout: 30_000 }, async () => {
const ex = analogExamples.find((e) => e.id === id);
expect(ex, `${id} not found in analogExamples`).toBeDefined();
const { netlist } = buildNetlist({
components: ex!.components.map((c) => ({
id: c.id,
metadataId: c.type.replace(/^wokwi-/, ''),
properties: c.properties ?? {},
})),
wires: ex!.wires.map((w) => ({
id: w.id,
start: { componentId: w.start.componentId, pinName: w.start.pinName },
end: { componentId: w.end.componentId, pinName: w.end.pinName },
})),
boards: [],
analysis: { kind: 'op' },
});
const result = await runNetlist(netlist);
expect(result.variableNames.length).toBeGreaterThan(0);
expect(Number.isFinite(result.dcValue(result.variableNames[0]))).toBe(true);
});
}
});

View File

@ -0,0 +1,132 @@
/**
* Analog examples — SPICE netlist validation.
*
* For each board-less analog example:
* 1. It's exported as boardFilter:'analog' (so the gallery groups it).
* 2. Its wires reference only components that actually exist.
* 3. buildNetlist produces a non-empty netlist with a ground node.
* 4. Every component metadataId used is actually mapped to SPICE.
*
* We deliberately do NOT run ngspice against every circuit here — that's
* ~30 × ~500 ms of WASM work per CI run. A handful of representative
* topologies are spot-checked for solver convergence; the rest rely on
* componentToSpice smoke-tests and NetlistBuilder unit tests for correctness.
*/
import { describe, it, expect } from 'vitest';
import { analogExamples } from '../data/examples-analog';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
import { mappedMetadataIds } from '../simulation/spice/componentToSpice';
function toSpiceComponents(example: (typeof analogExamples)[number]) {
return example.components.map((c) => ({
id: c.id,
metadataId: c.type.replace(/^wokwi-/, ''),
properties: c.properties ?? {},
}));
}
function toSpiceWires(example: (typeof analogExamples)[number]) {
return example.wires.map((w) => ({
id: w.id,
start: { componentId: w.start.componentId, pinName: w.start.pinName },
end: { componentId: w.end.componentId, pinName: w.end.pinName },
}));
}
describe('analogExamples — shape', () => {
it('exports exactly 30 board-less analog circuits', () => {
expect(analogExamples.length).toBe(30);
});
it('every example uses boardFilter: "analog" and category: "circuits"', () => {
for (const ex of analogExamples) {
expect(ex.boardFilter, `${ex.id} boardFilter`).toBe('analog');
expect(ex.category, `${ex.id} category`).toBe('circuits');
}
});
it('no example lists a board in its components[]', () => {
const BOARD_PREFIXES = ['wokwi-arduino-', 'wokwi-esp32', 'wokwi-raspberry-', 'wokwi-nano-rp'];
for (const ex of analogExamples) {
const boards = ex.components.filter((c) =>
BOARD_PREFIXES.some((p) => c.type.startsWith(p)),
);
expect(boards.map((b) => b.id), `${ex.id}`).toEqual([]);
}
});
it('no example sets a boardType (analog circuits are board-less)', () => {
for (const ex of analogExamples) {
expect(ex.boardType, `${ex.id}`).toBeUndefined();
}
});
it('every component type has a SPICE mapping', () => {
const mapped = new Set(mappedMetadataIds());
const unmapped = new Set<string>();
for (const ex of analogExamples) {
for (const c of ex.components) {
const id = c.type.replace(/^wokwi-/, '');
if (!mapped.has(id)) unmapped.add(`${ex.id}:${c.id}(${id})`);
}
}
expect(Array.from(unmapped)).toEqual([]);
});
it('every wire endpoint references a component that exists', () => {
for (const ex of analogExamples) {
const ids = new Set(ex.components.map((c) => c.id));
for (const w of ex.wires) {
expect(ids.has(w.start.componentId), `${ex.id}:${w.id}.start(${w.start.componentId})`).toBe(true);
expect(ids.has(w.end.componentId), `${ex.id}:${w.id}.end(${w.end.componentId})`).toBe(true);
}
}
});
it('every example has at least one signal-generator to seed ground', () => {
for (const ex of analogExamples) {
const hasSig = ex.components.some((c) => c.type === 'wokwi-signal-generator');
expect(hasSig, `${ex.id} has no signal-generator (no ground reference)`).toBe(true);
}
});
});
describe('analogExamples — netlist generation', () => {
it('each example produces a non-empty netlist with ground net', () => {
for (const ex of analogExamples) {
const { netlist } = buildNetlist({
components: toSpiceComponents(ex),
wires: toSpiceWires(ex),
boards: [],
analysis: { kind: 'op' },
});
expect(netlist.length, `${ex.id} netlist empty`).toBeGreaterThan(20);
expect(netlist, `${ex.id} missing .end`).toContain('.end');
// Every signal-generator must drop a V-source whose second node is 0
// (the GND pin is canonicalised). That's how we know ground is wired.
const sigs = ex.components.filter((c) => c.type === 'wokwi-signal-generator');
for (const sig of sigs) {
const re = new RegExp(`^V_${sig.id}\\s+\\S+\\s+0\\b`, 'm');
expect(netlist, `${ex.id}: ${sig.id} GND not canonicalised to 0`).toMatch(re);
}
}
});
it('each example emits at least one card referencing every component id', () => {
for (const ex of analogExamples) {
const { netlist } = buildNetlist({
components: toSpiceComponents(ex),
wires: toSpiceWires(ex),
boards: [],
analysis: { kind: 'op' },
});
for (const c of ex.components) {
// Cards begin with an element prefix (R/C/L/D/Q/M/E/S/V/B) followed by
// "_<id>" somewhere. The voltmeter e.g. emits "R_vm_vmR ...", so the
// id may be followed by any non-word-boundary character.
const re = new RegExp(`^[RCLDQMESVB]_${c.id}(?:_|\\b)`, 'm');
expect(netlist, `${ex.id} missing card for ${c.id} (${c.type})`).toMatch(re);
}
}
});
});

View File

@ -506,9 +506,9 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
{/* Compile */}
<button
onClick={handleCompile}
disabled={compiling}
disabled={compiling || !activeBoard}
className="tb-btn tb-btn-compile"
title={compiling ? 'Loading…' : activeBoard?.languageMode === 'micropython' ? 'Load MicroPython' : 'Compile (Ctrl+B)'}
title={!activeBoard ? 'Add a board to compile' : compiling ? 'Loading…' : activeBoard?.languageMode === 'micropython' ? 'Load MicroPython' : 'Compile (Ctrl+B)'}
>
{compiling ? (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="spin">
@ -526,9 +526,9 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
{/* Run */}
<button
onClick={handleRun}
disabled={running || compiling}
disabled={running || compiling || !activeBoard}
className="tb-btn tb-btn-run"
title={activeBoard?.languageMode === 'micropython' ? 'Run MicroPython' : 'Run (auto-compiles if needed)'}
title={!activeBoard ? 'Add a board to run' : activeBoard?.languageMode === 'micropython' ? 'Run MicroPython' : 'Run (auto-compiles if needed)'}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="5,3 19,12 5,21" />

View File

@ -194,13 +194,16 @@ export const CircuitPreview: React.FC<CircuitPreviewProps> = ({
items.push({ id: b.boardKind, x: b.x, y: b.y, def, fixed: true });
});
} else {
const boardKind = example.boardType ?? 'arduino-uno';
const boardDef = BOARD_DEFS[boardKind];
// Analog-only examples have no board at all — skip implicit board injection
// and board-def lookup entirely.
const isAnalogOnly = (example as any).boardFilter === 'analog';
const boardKind = example.boardType ?? (isAnalogOnly ? null : 'arduino-uno');
const boardDef = boardKind ? BOARD_DEFS[boardKind] : undefined;
// Check if the board is explicitly listed in components[]
const boardInComponents = example.components.some(c => isBoardType(c.type));
if (boardDef && !boardInComponents) {
if (boardDef && !boardInComponents && !isAnalogOnly) {
// Board is implicit (pico, esp32, nano examples) — inject at default position
// Position it to the left of the other components
const minCompX = example.components.length > 0

View File

@ -23,7 +23,7 @@ interface BoardTab {
}
const BOARD_TABS: BoardTab[] = [
{ id: 'all', label: 'All Boards', color: '#ffffff', bg: '#444444' },
{ id: 'all', label: 'All', color: '#ffffff', bg: '#444444' },
{ id: 'arduino-uno', label: 'Arduino Uno', color: '#ffffff', bg: '#007acc' },
{ id: 'arduino-nano', label: 'Arduino Nano', color: '#ffffff', bg: '#0055aa' },
{ id: 'arduino-mega', label: 'Arduino Mega', color: '#ffffff', bg: '#003388' },
@ -31,6 +31,7 @@ const BOARD_TABS: BoardTab[] = [
{ id: 'esp32', label: 'ESP32 (Xtensa)', color: '#ffffff', bg: '#e77d11' },
{ id: 'esp32-c3', label: 'ESP32-C3 (RISC-V)', color: '#ffffff', bg: '#27ae60' },
{ id: 'multi', label: 'Multi-Board', color: '#ffffff', bg: '#7b2d8b' },
{ id: 'analog', label: 'Analog', color: '#ffffff', bg: '#0ea5a5' },
];
function getBoardFilter(example: ExampleProject): string {

View File

@ -231,6 +231,20 @@ const SignalGenerator: React.FC<InlineSVGProps> = ({ w, h }) => (
</svg>
);
function meterGlyph(label: string, w: number, h: number): React.ReactElement {
return (
<svg width={w} height={h} viewBox="0 0 72 56" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="68" height="52" rx="4" fill="#111827" stroke="#e5e7eb" strokeWidth="1.2"/>
<rect x="8" y="8" width="56" height="22" rx="2" fill="#0b1220" stroke="#22d3ee" strokeWidth="1"/>
<text x="36" y="24" textAnchor="middle" fontSize="10" fill="#22d3ee" fontFamily="monospace">{label}</text>
<line x1="0" y1="48" x2="8" y2="48" stroke="#888" strokeWidth="2"/>
<line x1="64" y1="48" x2="72" y2="48" stroke="#888" strokeWidth="2"/>
</svg>
);
}
const Voltmeter: React.FC<InlineSVGProps> = ({ w, h }) => meterGlyph('V', w, h);
const Ammeter: React.FC<InlineSVGProps> = ({ w, h }) => meterGlyph('A', w, h);
// ─── Registry ──────────────────────────────────────────────────────────────
interface InlineEntry {
component: React.FC<InlineSVGProps>;
@ -300,4 +314,7 @@ export const INLINE_SVGS: Record<string, InlineEntry> = {
'wokwi-battery-9v': { component: Battery9V, w: 48, h: 72 },
'wokwi-battery-aa': { component: Battery9V, w: 48, h: 72 },
'wokwi-signal-generator': { component: SignalGenerator, w: 80, h: 64 },
// Instruments
'wokwi-instr-voltmeter': { component: Voltmeter, w: 72, h: 56 },
'wokwi-instr-ammeter': { component: Ammeter, w: 72, h: 56 },
};

View File

@ -1329,18 +1329,28 @@ export const SimulatorCanvas = () => {
{/* Status LED */}
<span className={`status-dot ${running ? 'running' : 'stopped'}`} title={running ? 'Running' : 'Stopped'} />
{/* Active board selector (multi-board) */}
<select
className="board-selector"
value={activeBoardId ?? ''}
onChange={(e) => useSimulatorStore.getState().setActiveBoardId(e.target.value)}
disabled={running}
title="Active board"
>
{boards.map((b) => (
<option key={b.id} value={b.id}>{BOARD_KIND_LABELS[b.boardKind] ?? b.id}</option>
))}
</select>
{/* Active board selector (multi-board) — hidden when no boards */}
{boards.length > 0 ? (
<select
className="board-selector"
value={activeBoardId ?? ''}
onChange={(e) => useSimulatorStore.getState().setActiveBoardId(e.target.value)}
disabled={running}
title="Active board"
>
{boards.map((b) => (
<option key={b.id} value={b.id}>{BOARD_KIND_LABELS[b.boardKind] ?? b.id}</option>
))}
</select>
) : (
<span
className="board-selector"
style={{ opacity: 0.55, fontStyle: 'italic', cursor: 'default' }}
title="No board on canvas — add one with the Add button to compile and run code"
>
No board
</span>
)}
{/* Serial Monitor toggle */}
<button

View File

@ -0,0 +1,883 @@
/**
* Analog-only circuit examples (no Arduino board).
*
* These 30 examples exercise the ngspice-WASM SPICE backend with standalone
* analog circuits: passive dividers & filters, diode rectifiers/clippers,
* BJT/MOSFET stages, op-amp topologies. Every circuit uses a
* `signal-generator` as the source (and ground reference, since its `GND`
* pin is the only one canonicalised to SPICE node 0 — `battery-9v −` is not).
*
* All circuits are tagged `boardFilter: 'analog'` so the gallery can group
* them together and so the preview / loader know to skip the implicit
* Arduino board.
*
* Layout note: each circuit fits inside roughly 560 × 320 px so the
* CircuitPreview cards render cleanly. Component sizes come from
* InlineComponentSVGs.tsx / COMP_DEFS in CircuitPreview.tsx.
*/
import type { ExampleProject } from './examples';
// ── Helpers ────────────────────────────────────────────────────────────────
function w(
id: string,
from: [string, string],
to: [string, string],
color = '#00aaff',
) {
return {
id,
start: { componentId: from[0], pinName: from[1] },
end: { componentId: to[0], pinName: to[1] },
color,
};
}
// Colors used throughout
const C_PWR = '#ff3b3b'; // power / V+
const C_GND = '#000000'; // ground
const C_SIG = '#ffaa00'; // signal
const C_OUT = '#22d3ee'; // output
const C_WIRE = '#00aaff'; // generic signal
// Standard placeholder sketch text so the editor has something non-empty
// when the user loads an analog example.
const ANALOG_SKETCH = `// Analog-only circuit — no Arduino sketch required.
// Toggle the electrical-simulation (⚡) button to run the SPICE engine.
void setup() {}
void loop() {}
`;
// Base example skeleton — cuts down per-circuit boilerplate.
function analog(
id: string,
title: string,
description: string,
difficulty: ExampleProject['difficulty'],
components: ExampleProject['components'],
wires: ExampleProject['wires'],
): ExampleProject {
return {
id,
title,
description,
category: 'circuits',
difficulty,
boardFilter: 'analog',
code: ANALOG_SKETCH,
components,
wires,
};
}
// ── Component shortcuts ────────────────────────────────────────────────────
function sigGen(id: string, x: number, y: number, props: Record<string, any>) {
return { type: 'wokwi-signal-generator', id, x, y, properties: props };
}
function res(id: string, x: number, y: number, value: string) {
return { type: 'wokwi-resistor', id, x, y, properties: { value } };
}
function cap(id: string, x: number, y: number, value: string) {
return { type: 'wokwi-capacitor', id, x, y, properties: { value } };
}
function ind(id: string, x: number, y: number, value: string) {
return { type: 'wokwi-inductor', id, x, y, properties: { value } };
}
function diode(id: string, x: number, y: number, kind = 'wokwi-diode-1n4007') {
return { type: kind, id, x, y, properties: {} };
}
function zener(id: string, x: number, y: number) {
return { type: 'wokwi-zener-1n4733', id, x, y, properties: {} };
}
function bjtNpn(id: string, x: number, y: number) {
return { type: 'wokwi-bjt-2n2222', id, x, y, properties: {} };
}
function bjtPnp(id: string, x: number, y: number) {
return { type: 'wokwi-bjt-bc557', id, x, y, properties: {} };
}
function mosN(id: string, x: number, y: number) {
return { type: 'wokwi-mosfet-2n7000', id, x, y, properties: {} };
}
function mosP(id: string, x: number, y: number) {
return { type: 'wokwi-mosfet-irf9540', id, x, y, properties: {} };
}
function opamp(id: string, x: number, y: number) {
return { type: 'wokwi-opamp-lm358', id, x, y, properties: {} };
}
function vmeter(id: string, x: number, y: number) {
return { type: 'wokwi-instr-voltmeter', id, x, y, properties: {} };
}
function ammeter(id: string, x: number, y: number) {
return { type: 'wokwi-instr-ammeter', id, x, y, properties: {} };
}
// ── Examples ───────────────────────────────────────────────────────────────
export const analogExamples: ExampleProject[] = [
// ════════════════════════════════════════════════════════════════════════
// PASSIVE (1–7)
// ════════════════════════════════════════════════════════════════════════
analog(
'an-voltage-divider',
'Voltage Divider',
'Two resistors divide a 5 V DC source in half. Classic first SPICE demo — expect 2.5 V across R2.',
'beginner',
[
sigGen('src', 60, 160, { waveform: 'dc', offset: 5, amplitude: 0, frequency: 1 }),
res('r1', 260, 120, '10000'),
res('r2', 260, 220, '10000'),
vmeter('vm', 420, 180),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_PWR),
w('w2', ['r1', '2'], ['r2', '1'], C_SIG),
w('w3', ['r2', '2'], ['src', 'GND'], C_GND),
w('w4', ['r1', '2'], ['vm', 'V+'], C_OUT),
w('w5', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-series-resistors',
'Series Resistors',
'Three resistors in series carry the same current — ammeter reads V/(R1+R2+R3).',
'beginner',
[
sigGen('src', 60, 160, { waveform: 'dc', offset: 10, amplitude: 0, frequency: 1 }),
res('r1', 220, 120, '1000'),
res('r2', 360, 120, '2200'),
res('r3', 500, 120, '4700'),
ammeter('am', 300, 240),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_PWR),
w('w2', ['r1', '2'], ['r2', '1'], C_WIRE),
w('w3', ['r2', '2'], ['r3', '1'], C_WIRE),
w('w4', ['r3', '2'], ['am', 'A+'], C_SIG),
w('w5', ['am', 'A-'], ['src', 'GND'], C_GND),
],
),
analog(
'an-parallel-resistors',
'Parallel Resistors',
'Three resistors in parallel — total resistance is the reciprocal of the reciprocal sum. Ammeter shows total current.',
'beginner',
[
sigGen('src', 60, 180, { waveform: 'dc', offset: 5, amplitude: 0, frequency: 1 }),
res('r1', 320, 100, '1000'),
res('r2', 320, 180, '1000'),
res('r3', 320, 260, '1000'),
ammeter('am', 180, 280),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_PWR),
w('w2', ['src', 'SIG'], ['r2', '1'], C_PWR),
w('w3', ['src', 'SIG'], ['r3', '1'], C_PWR),
w('w4', ['r1', '2'], ['am', 'A+'], C_SIG),
w('w5', ['r2', '2'], ['am', 'A+'], C_SIG),
w('w6', ['r3', '2'], ['am', 'A+'], C_SIG),
w('w7', ['am', 'A-'], ['src', 'GND'], C_GND),
],
),
analog(
'an-rc-low-pass',
'RC Low-Pass Filter',
'1 kHz sine through R = 1.6 kΩ and C = 100 nF. fc ≈ 1 kHz — output attenuated by −3 dB.',
'beginner',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 1000, amplitude: 1, offset: 0 }),
res('r1', 260, 140, '1600'),
cap('c1', 400, 240, '100n'),
vmeter('vm', 520, 180),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_SIG),
w('w2', ['r1', '2'], ['c1', '1'], C_OUT),
w('w3', ['c1', '2'], ['src', 'GND'], C_GND),
w('w4', ['r1', '2'], ['vm', 'V+'], C_OUT),
w('w5', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-rc-high-pass',
'RC High-Pass Filter',
'Swap the R and C of a low-pass and you get a high-pass. 1 kHz sine through C — R — GND.',
'beginner',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 1000, amplitude: 1, offset: 0 }),
cap('c1', 240, 140, '100n'),
res('r1', 400, 240, '1600'),
vmeter('vm', 520, 180),
],
[
w('w1', ['src', 'SIG'], ['c1', '1'], C_SIG),
w('w2', ['c1', '2'], ['r1', '1'], C_OUT),
w('w3', ['r1', '2'], ['src', 'GND'], C_GND),
w('w4', ['c1', '2'], ['vm', 'V+'], C_OUT),
w('w5', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-rl-low-pass',
'RL Low-Pass Filter',
'Inductor in series, resistor to ground. High frequencies dropped across L; low frequencies pass through.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 5000, amplitude: 1, offset: 0 }),
ind('l1', 240, 140, '10m'),
res('r1', 400, 240, '1000'),
vmeter('vm', 520, 180),
],
[
w('w1', ['src', 'SIG'], ['l1', '1'], C_SIG),
w('w2', ['l1', '2'], ['r1', '1'], C_OUT),
w('w3', ['r1', '2'], ['src', 'GND'], C_GND),
w('w4', ['l1', '2'], ['vm', 'V+'], C_OUT),
w('w5', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-rlc-series-resonance',
'Series RLC Resonant Circuit',
'L = 1 mH, C = 1 µF gives fr ≈ 5 kHz. Sweep the signal generator — Vc peaks at resonance (Q-dependent).',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 5000, amplitude: 1, offset: 0 }),
res('r1', 240, 140, '10'),
ind('l1', 360, 140, '1m'),
cap('c1', 500, 240, '1u'),
vmeter('vm', 620, 180),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_SIG),
w('w2', ['r1', '2'], ['l1', '1'], C_WIRE),
w('w3', ['l1', '2'], ['c1', '1'], C_OUT),
w('w4', ['c1', '2'], ['src', 'GND'], C_GND),
w('w5', ['l1', '2'], ['vm', 'V+'], C_OUT),
w('w6', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
// ════════════════════════════════════════════════════════════════════════
// DIODES (8–14)
// ════════════════════════════════════════════════════════════════════════
analog(
'an-half-wave-rectifier',
'Half-Wave Rectifier',
'Single diode passes only the positive half-cycle of a 50 Hz sine. Load resistor drops the rectified voltage.',
'beginner',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 50, amplitude: 8, offset: 0 }),
diode('d1', 240, 140),
res('r1', 420, 240, '1000'),
vmeter('vm', 560, 180),
],
[
w('w1', ['src', 'SIG'], ['d1', 'A'], C_SIG),
w('w2', ['d1', 'C'], ['r1', '1'], C_OUT),
w('w3', ['r1', '2'], ['src', 'GND'], C_GND),
w('w4', ['d1', 'C'], ['vm', 'V+'], C_OUT),
w('w5', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-bridge-rectifier',
'Full-Wave Bridge Rectifier',
'Four diodes in a bridge rectify both halves of the AC input. Load R1 sees full-wave pulses at 2× the input frequency.',
'intermediate',
[
sigGen('src', 60, 200, { waveform: 'sine', frequency: 50, amplitude: 10, offset: 0 }),
diode('d1', 260, 100),
diode('d2', 380, 100),
diode('d3', 260, 280),
diode('d4', 380, 280),
res('r1', 540, 200, '2200'),
vmeter('vm', 680, 200),
],
[
// Bridge: SIG -> d1 anode, d3 cathode; GND -> d2 anode, d4 cathode
// + rail (top): d1 cathode, d2 cathode
// − rail (bottom): d3 anode, d4 anode
w('w1', ['src', 'SIG'], ['d1', 'A'], C_SIG),
w('w2', ['src', 'SIG'], ['d3', 'C'], C_SIG),
w('w3', ['src', 'GND'], ['d2', 'A'], C_GND),
w('w4', ['src', 'GND'], ['d4', 'C'], C_GND),
w('w5', ['d1', 'C'], ['d2', 'C'], C_OUT),
w('w6', ['d3', 'A'], ['d4', 'A'], '#666666'),
w('w7', ['d1', 'C'], ['r1', '1'], C_OUT),
w('w8', ['r1', '2'], ['d3', 'A'], '#666666'),
w('w9', ['r1', '1'], ['vm', 'V+'], C_OUT),
w('w10', ['r1', '2'], ['vm', 'V-'], '#666666'),
],
),
analog(
'an-smoothed-rectifier',
'Smoothed Half-Wave Rectifier',
'Half-wave rectifier followed by a 100 µF reservoir cap. Output is near-DC with ripple inversely proportional to C.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 50, amplitude: 10, offset: 0 }),
diode('d1', 240, 140),
cap('c1', 400, 240, '100u'),
res('r1', 540, 240, '1000'),
vmeter('vm', 680, 180),
],
[
w('w1', ['src', 'SIG'], ['d1', 'A'], C_SIG),
w('w2', ['d1', 'C'], ['c1', '1'], C_OUT),
w('w3', ['c1', '1'], ['r1', '1'], C_OUT),
w('w4', ['c1', '2'], ['src', 'GND'], C_GND),
w('w5', ['r1', '2'], ['src', 'GND'], C_GND),
w('w6', ['c1', '1'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-zener-regulator',
'Zener Shunt Regulator',
'Series resistor + reverse-biased zener clamps the output to Vz (5.1 V for a 1N4733) despite changing input.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
res('r1', 240, 140, '220'),
zener('d1', 400, 240),
res('rl', 540, 240, '2200'),
vmeter('vm', 680, 180),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_PWR),
w('w2', ['r1', '2'], ['d1', 'C'], C_OUT),
w('w3', ['r1', '2'], ['rl', '1'], C_OUT),
w('w4', ['d1', 'A'], ['src', 'GND'], C_GND),
w('w5', ['rl', '2'], ['src', 'GND'], C_GND),
w('w6', ['r1', '2'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-diode-clipper',
'Diode Clipper',
'Two diodes to VCC/GND via a series resistor limit the output swing to roughly ±0.7 V — symmetric clipping.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 1000, amplitude: 5, offset: 0 }),
res('r1', 240, 180, '1000'),
diode('d1', 400, 100),
diode('d2', 400, 280),
vmeter('vm', 560, 180),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_SIG),
w('w2', ['r1', '2'], ['d1', 'A'], C_OUT),
w('w3', ['r1', '2'], ['d2', 'C'], C_OUT),
w('w4', ['d1', 'C'], ['src', 'GND'], C_GND), // upper clip to 0 (really D inside ±0.7 both sides)
w('w5', ['d2', 'A'], ['src', 'GND'], C_GND),
w('w6', ['r1', '2'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-diode-clamper',
'Diode Clamper (DC Restorer)',
'Series cap + parallel diode shifts the signal so its negative peak sits at 0 V. Output swings 0 → 2·Vpeak.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 1000, amplitude: 5, offset: 0 }),
cap('c1', 240, 180, '1u'),
diode('d1', 400, 240),
res('rl', 540, 240, '10000'),
vmeter('vm', 680, 180),
],
[
w('w1', ['src', 'SIG'], ['c1', '1'], C_SIG),
w('w2', ['c1', '2'], ['d1', 'C'], C_OUT),
w('w3', ['c1', '2'], ['rl', '1'], C_OUT),
w('w4', ['d1', 'A'], ['src', 'GND'], C_GND),
w('w5', ['rl', '2'], ['src', 'GND'], C_GND),
w('w6', ['c1', '2'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-voltage-doubler',
'Voltage Doubler (Villard / Greinacher)',
'Two caps + two diodes extract roughly 2·Vpeak DC from an AC input. First stage clamps, second stage peak-detects.',
'advanced',
[
sigGen('src', 60, 200, { waveform: 'sine', frequency: 50, amplitude: 8, offset: 0 }),
cap('c1', 220, 200, '10u'),
diode('d1', 360, 120),
diode('d2', 500, 200),
cap('c2', 640, 280, '100u'),
res('rl', 780, 280, '10000'),
vmeter('vm', 860, 160),
],
[
w('w1', ['src', 'SIG'], ['c1', '1'], C_SIG),
w('w2', ['c1', '2'], ['d1', 'A'], C_WIRE),
w('w3', ['d1', 'C'], ['src', 'GND'], C_GND),
w('w4', ['c1', '2'], ['d2', 'A'], C_WIRE),
w('w5', ['d2', 'C'], ['c2', '1'], C_OUT),
w('w6', ['c2', '1'], ['rl', '1'], C_OUT),
w('w7', ['c2', '2'], ['src', 'GND'], C_GND),
w('w8', ['rl', '2'], ['src', 'GND'], C_GND),
w('w9', ['c2', '1'], ['vm', 'V+'], C_OUT),
w('w10', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
// ════════════════════════════════════════════════════════════════════════
// BJT (15–20)
// ════════════════════════════════════════════════════════════════════════
analog(
'an-bjt-common-emitter',
'BJT Common-Emitter Amplifier',
'Classic voltage amplifier: base biased by a divider, emitter degenerated by Re, collector resistor sets gain.',
'advanced',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 240, { waveform: 'sine', frequency: 1000, amplitude: 0.05, offset: 0 }),
res('rb1', 240, 60, '47000'),
res('rb2', 240, 200, '10000'),
res('rc', 440, 60, '4700'),
res('re', 440, 300, '1000'),
cap('cin', 220, 260, '10u'),
bjtNpn('q1', 360, 160),
vmeter('vm', 600, 120),
],
[
w('w1', ['vcc', 'SIG'], ['rb1', '1'], C_PWR),
w('w2', ['rb1', '2'], ['rb2', '1'], C_WIRE),
w('w3', ['rb2', '2'], ['vcc', 'GND'], C_GND),
w('w4', ['rb1', '2'], ['q1', 'B'], C_SIG),
w('w5', ['vcc', 'SIG'], ['rc', '1'], C_PWR),
w('w6', ['rc', '2'], ['q1', 'C'], C_OUT),
w('w7', ['q1', 'E'], ['re', '1'], C_WIRE),
w('w8', ['re', '2'], ['vcc', 'GND'], C_GND),
w('w9', ['src', 'SIG'], ['cin', '1'], C_SIG),
w('w10', ['cin', '2'], ['q1', 'B'], C_SIG),
w('w11', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w12', ['q1', 'C'], ['vm', 'V+'], C_OUT),
w('w13', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-bjt-emitter-follower',
'BJT Emitter Follower',
'Common-collector buffer: unity voltage gain, high input impedance, low output impedance. Great as a driver stage.',
'intermediate',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 240, { waveform: 'sine', frequency: 1000, amplitude: 1, offset: 6 }),
res('rb', 260, 80, '100000'),
bjtNpn('q1', 420, 160),
res('re', 500, 300, '1000'),
vmeter('vm', 640, 220),
],
[
w('w1', ['vcc', 'SIG'], ['q1', 'C'], C_PWR),
w('w2', ['src', 'SIG'], ['rb', '1'], C_SIG),
w('w3', ['rb', '2'], ['q1', 'B'], C_SIG),
w('w4', ['q1', 'E'], ['re', '1'], C_OUT),
w('w5', ['re', '2'], ['vcc', 'GND'], C_GND),
w('w6', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w7', ['q1', 'E'], ['vm', 'V+'], C_OUT),
w('w8', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-bjt-switch',
'BJT as a Switch',
'Base driven into saturation through Rb; collector pulled up via a load resistor. DC step input toggles Vce between ~0.2 V and Vcc.',
'beginner',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 9, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 240, { waveform: 'square', frequency: 100, amplitude: 2, offset: 2 }),
res('rb', 260, 240, '4700'),
res('rl', 440, 60, '1000'),
bjtNpn('q1', 420, 160),
vmeter('vm', 600, 160),
],
[
w('w1', ['vcc', 'SIG'], ['rl', '1'], C_PWR),
w('w2', ['rl', '2'], ['q1', 'C'], C_OUT),
w('w3', ['src', 'SIG'], ['rb', '1'], C_SIG),
w('w4', ['rb', '2'], ['q1', 'B'], C_SIG),
w('w5', ['q1', 'E'], ['vcc', 'GND'], C_GND),
w('w6', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w7', ['q1', 'C'], ['vm', 'V+'], C_OUT),
w('w8', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-darlington',
'Darlington Pair',
'Two cascaded NPNs multiply β — β_total ≈ β1·β2. Used when a single transistor can’t supply enough base current for a heavy load.',
'advanced',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 260, { waveform: 'dc', offset: 2, amplitude: 0, frequency: 1 }),
res('rb', 260, 200, '10000'),
bjtNpn('q1', 360, 140),
bjtNpn('q2', 500, 200),
res('rl', 620, 80, '220'),
vmeter('vm', 720, 180),
],
[
w('w1', ['vcc', 'SIG'], ['rl', '1'], C_PWR),
w('w2', ['rl', '2'], ['q1', 'C'], C_OUT),
w('w3', ['q1', 'C'], ['q2', 'C'], C_PWR),
w('w4', ['src', 'SIG'], ['rb', '1'], C_SIG),
w('w5', ['rb', '2'], ['q1', 'B'], C_SIG),
w('w6', ['q1', 'E'], ['q2', 'B'], C_WIRE),
w('w7', ['q2', 'E'], ['vcc', 'GND'], C_GND),
w('w8', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w9', ['q2', 'C'], ['vm', 'V+'], C_OUT),
w('w10', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-current-mirror',
'NPN Current Mirror',
'Reference current in Q1 is mirrored through Q2’s collector — matched transistors + shared Vbe make the collector currents equal.',
'advanced',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
res('rref', 240, 40, '10000'),
bjtNpn('q1', 320, 160),
bjtNpn('q2', 520, 160),
res('rload', 620, 40, '4700'),
ammeter('am', 720, 160),
],
[
w('w1', ['vcc', 'SIG'], ['rref', '1'], C_PWR),
w('w2', ['rref', '2'], ['q1', 'C'], C_WIRE),
w('w3', ['q1', 'C'], ['q1', 'B'], C_WIRE),
w('w4', ['q1', 'B'], ['q2', 'B'], C_WIRE),
w('w5', ['vcc', 'SIG'], ['rload', '1'], C_PWR),
w('w6', ['rload', '2'], ['am', 'A+'], C_OUT),
w('w7', ['am', 'A-'], ['q2', 'C'], C_OUT),
w('w8', ['q1', 'E'], ['vcc', 'GND'], C_GND),
w('w9', ['q2', 'E'], ['vcc', 'GND'], C_GND),
],
),
analog(
'an-bjt-diff-pair',
'BJT Differential Pair',
'Long-tailed pair: a shared emitter tail resistor steers current between Q1 and Q2 according to the differential input.',
'advanced',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('in1', 60, 220, { waveform: 'sine', frequency: 1000, amplitude: 0.02, offset: 2.5 }),
sigGen('in2', 60, 360, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
res('rc1', 300, 40, '4700'),
res('rc2', 500, 40, '4700'),
bjtNpn('q1', 280, 180),
bjtNpn('q2', 500, 180),
res('rtail', 400, 320, '4700'),
vmeter('vm', 620, 120),
],
[
w('w1', ['vcc', 'SIG'], ['rc1', '1'], C_PWR),
w('w2', ['vcc', 'SIG'], ['rc2', '1'], C_PWR),
w('w3', ['rc1', '2'], ['q1', 'C'], C_WIRE),
w('w4', ['rc2', '2'], ['q2', 'C'], C_OUT),
w('w5', ['in1', 'SIG'], ['q1', 'B'], C_SIG),
w('w6', ['in2', 'SIG'], ['q2', 'B'], C_SIG),
w('w7', ['q1', 'E'], ['rtail', '1'], C_WIRE),
w('w8', ['q2', 'E'], ['rtail', '1'], C_WIRE),
w('w9', ['rtail', '2'], ['vcc', 'GND'], C_GND),
w('w10', ['in1', 'GND'], ['vcc', 'GND'], C_GND),
w('w11', ['in2', 'GND'], ['vcc', 'GND'], C_GND),
w('w12', ['q2', 'C'], ['vm', 'V+'], C_OUT),
w('w13', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
// ════════════════════════════════════════════════════════════════════════
// MOSFET (21–23)
// ════════════════════════════════════════════════════════════════════════
analog(
'an-mosfet-switch',
'MOSFET Low-Side Switch',
'2N7000 NMOS with a pull-down gate resistor. Gate drive above Vth turns the device on and sinks current from the load to GND.',
'beginner',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 260, { waveform: 'square', frequency: 100, amplitude: 2, offset: 2 }),
res('rg', 240, 240, '100'),
res('rl', 420, 60, '470'),
mosN('m1', 420, 160),
res('rgp', 240, 340, '100000'),
vmeter('vm', 600, 160),
],
[
w('w1', ['vcc', 'SIG'], ['rl', '1'], C_PWR),
w('w2', ['rl', '2'], ['m1', 'D'], C_OUT),
w('w3', ['src', 'SIG'], ['rg', '1'], C_SIG),
w('w4', ['rg', '2'], ['m1', 'G'], C_SIG),
w('w5', ['m1', 'G'], ['rgp', '1'], C_SIG),
w('w6', ['rgp', '2'], ['vcc', 'GND'], C_GND),
w('w7', ['m1', 'S'], ['vcc', 'GND'], C_GND),
w('w8', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w9', ['m1', 'D'], ['vm', 'V+'], C_OUT),
w('w10', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-mosfet-common-source',
'MOSFET Common-Source Amplifier',
'2N7000 as a small-signal amp: drain resistor sets gain, source resistor stabilises bias. AC-coupled signal input.',
'advanced',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('src', 60, 260, { waveform: 'sine', frequency: 1000, amplitude: 0.1, offset: 0 }),
res('rg1', 240, 40, '1000000'),
res('rg2', 240, 200, '470000'),
res('rd', 420, 40, '4700'),
res('rs', 420, 300, '1000'),
cap('cin', 220, 260, '10u'),
mosN('m1', 380, 160),
vmeter('vm', 560, 120),
],
[
w('w1', ['vcc', 'SIG'], ['rg1', '1'], C_PWR),
w('w2', ['rg1', '2'], ['rg2', '1'], C_WIRE),
w('w3', ['rg2', '2'], ['vcc', 'GND'], C_GND),
w('w4', ['rg1', '2'], ['m1', 'G'], C_SIG),
w('w5', ['vcc', 'SIG'], ['rd', '1'], C_PWR),
w('w6', ['rd', '2'], ['m1', 'D'], C_OUT),
w('w7', ['m1', 'S'], ['rs', '1'], C_WIRE),
w('w8', ['rs', '2'], ['vcc', 'GND'], C_GND),
w('w9', ['src', 'SIG'], ['cin', '1'], C_SIG),
w('w10', ['cin', '2'], ['m1', 'G'], C_SIG),
w('w11', ['src', 'GND'], ['vcc', 'GND'], C_GND),
w('w12', ['m1', 'D'], ['vm', 'V+'], C_OUT),
w('w13', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-mosfet-pmos-highside',
'PMOS High-Side Switch',
'P-channel MOSFET (IRF9540) sources current from VCC when the gate is pulled below VCC−|Vth|. Ideal for load switching.',
'intermediate',
[
sigGen('vcc', 60, 40, { waveform: 'dc', offset: 12, amplitude: 0, frequency: 1 }),
sigGen('ctrl', 60, 260, { waveform: 'square', frequency: 5, amplitude: 6, offset: 6 }),
res('rg', 240, 220, '1000'),
mosP('m1', 420, 140),
res('rl', 540, 260, '220'),
vmeter('vm', 680, 260),
],
[
w('w1', ['vcc', 'SIG'], ['m1', 'S'], C_PWR),
w('w2', ['ctrl', 'SIG'], ['rg', '1'], C_SIG),
w('w3', ['rg', '2'], ['m1', 'G'], C_SIG),
w('w4', ['m1', 'D'], ['rl', '1'], C_OUT),
w('w5', ['rl', '2'], ['vcc', 'GND'], C_GND),
w('w6', ['ctrl', 'GND'], ['vcc', 'GND'], C_GND),
w('w7', ['m1', 'D'], ['vm', 'V+'], C_OUT),
w('w8', ['vcc', 'GND'], ['vm', 'V-'], C_GND),
],
),
// ════════════════════════════════════════════════════════════════════════
// OP-AMPS (24–30)
// ════════════════════════════════════════════════════════════════════════
analog(
'an-opamp-inverting',
'Op-Amp Inverting Amplifier',
'Gain = −Rf/Rin. Rf = 10 k, Rin = 1 k → gain = −10. LM358 single-supply model clamps the output to [0, VCC−1.5].',
'intermediate',
[
sigGen('src', 60, 200, { waveform: 'sine', frequency: 500, amplitude: 0.2, offset: 2.5 }),
res('rin', 240, 160, '1000'),
res('rf', 420, 60, '10000'),
opamp('u1', 400, 180),
sigGen('vref', 60, 360, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
vmeter('vm', 580, 180),
],
[
w('w1', ['src', 'SIG'], ['rin', '1'], C_SIG),
w('w2', ['rin', '2'], ['u1', 'IN-'], C_SIG),
w('w3', ['u1', 'IN-'], ['rf', '1'], C_SIG),
w('w4', ['rf', '2'], ['u1', 'OUT'], C_OUT),
w('w5', ['vref', 'SIG'], ['u1', 'IN+'], C_WIRE),
w('w6', ['vref', 'GND'], ['src', 'GND'], C_GND),
w('w7', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w8', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-opamp-non-inverting',
'Op-Amp Non-Inverting Amplifier',
'Gain = 1 + Rf/Rg. Rf = 10 k, Rg = 1 k → gain = +11. Output in-phase with input.',
'intermediate',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 500, amplitude: 0.1, offset: 2.5 }),
opamp('u1', 280, 180),
res('rf', 460, 80, '10000'),
res('rg', 460, 280, '1000'),
vmeter('vm', 600, 180),
],
[
w('w1', ['src', 'SIG'], ['u1', 'IN+'], C_SIG),
w('w2', ['u1', 'OUT'], ['rf', '1'], C_OUT),
w('w3', ['rf', '2'], ['u1', 'IN-'], C_WIRE),
w('w4', ['u1', 'IN-'], ['rg', '1'], C_WIRE),
w('w5', ['rg', '2'], ['src', 'GND'], C_GND),
w('w6', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-opamp-follower',
'Op-Amp Voltage Follower',
'Unity-gain buffer: output tracks the non-inverting input exactly. Infinite input impedance, near-zero output impedance.',
'beginner',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 200, amplitude: 1, offset: 2.5 }),
opamp('u1', 280, 180),
res('rl', 460, 260, '1000'),
vmeter('vm', 600, 180),
],
[
w('w1', ['src', 'SIG'], ['u1', 'IN+'], C_SIG),
w('w2', ['u1', 'OUT'], ['u1', 'IN-'], C_WIRE),
w('w3', ['u1', 'OUT'], ['rl', '1'], C_OUT),
w('w4', ['rl', '2'], ['src', 'GND'], C_GND),
w('w5', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w6', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-opamp-summing',
'Op-Amp Summing Amplifier',
'Virtual-ground summing junction: V_out = −Rf · (V1/R1 + V2/R2). Two-input mixer with unity gain per channel.',
'advanced',
[
sigGen('v1', 60, 100, { waveform: 'dc', offset: 1, amplitude: 0, frequency: 1 }),
sigGen('v2', 60, 260, { waveform: 'dc', offset: 2, amplitude: 0, frequency: 1 }),
res('r1', 240, 100, '10000'),
res('r2', 240, 260, '10000'),
res('rf', 420, 40, '10000'),
opamp('u1', 420, 160),
sigGen('vref', 60, 400, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
vmeter('vm', 600, 180),
],
[
w('w1', ['v1', 'SIG'], ['r1', '1'], C_SIG),
w('w2', ['v2', 'SIG'], ['r2', '1'], C_SIG),
w('w3', ['r1', '2'], ['u1', 'IN-'], C_WIRE),
w('w4', ['r2', '2'], ['u1', 'IN-'], C_WIRE),
w('w5', ['u1', 'IN-'], ['rf', '1'], C_WIRE),
w('w6', ['rf', '2'], ['u1', 'OUT'], C_OUT),
w('w7', ['vref', 'SIG'], ['u1', 'IN+'], C_WIRE),
w('w8', ['v1', 'GND'], ['v2', 'GND'], C_GND),
w('w9', ['v2', 'GND'], ['vref', 'GND'], C_GND),
w('w10', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w11', ['v1', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-opamp-integrator',
'Op-Amp Integrator',
'Rf replaced by a cap: V_out = −(1/RC) ∫V_in dt. Square-wave in → triangle-wave out. Classic analog-computer building block.',
'advanced',
[
sigGen('src', 60, 200, { waveform: 'square', frequency: 200, amplitude: 1, offset: 2.5 }),
res('rin', 240, 160, '10000'),
cap('cf', 420, 60, '100n'),
opamp('u1', 400, 180),
sigGen('vref', 60, 360, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
vmeter('vm', 580, 180),
],
[
w('w1', ['src', 'SIG'], ['rin', '1'], C_SIG),
w('w2', ['rin', '2'], ['u1', 'IN-'], C_SIG),
w('w3', ['u1', 'IN-'], ['cf', '1'], C_SIG),
w('w4', ['cf', '2'], ['u1', 'OUT'], C_OUT),
w('w5', ['vref', 'SIG'], ['u1', 'IN+'], C_WIRE),
w('w6', ['vref', 'GND'], ['src', 'GND'], C_GND),
w('w7', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w8', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-opamp-comparator',
'Op-Amp Comparator',
'Open-loop op-amp compares V_in against a reference. Output saturates high when IN+ > IN-, low otherwise.',
'intermediate',
[
sigGen('src', 60, 140, { waveform: 'sine', frequency: 100, amplitude: 2.5, offset: 2.5 }),
sigGen('vref', 60, 300, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
opamp('u1', 280, 200),
res('rl', 460, 260, '10000'),
vmeter('vm', 600, 200),
],
[
w('w1', ['src', 'SIG'], ['u1', 'IN+'], C_SIG),
w('w2', ['vref', 'SIG'], ['u1', 'IN-'], C_WIRE),
w('w3', ['u1', 'OUT'], ['rl', '1'], C_OUT),
w('w4', ['rl', '2'], ['src', 'GND'], C_GND),
w('w5', ['vref', 'GND'], ['src', 'GND'], C_GND),
w('w6', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w7', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
analog(
'an-schmitt-trigger',
'Op-Amp Schmitt Trigger',
'Positive feedback (R2 from OUT to IN+) adds hysteresis: two switching thresholds instead of one. Cleans up noisy signals.',
'advanced',
[
sigGen('src', 60, 180, { waveform: 'sine', frequency: 100, amplitude: 2.5, offset: 2.5 }),
sigGen('vref', 60, 360, { waveform: 'dc', offset: 2.5, amplitude: 0, frequency: 1 }),
res('r1', 240, 220, '10000'),
opamp('u1', 360, 180),
res('r2', 520, 100, '10000'),
vmeter('vm', 620, 220),
],
[
w('w1', ['src', 'SIG'], ['r1', '1'], C_SIG),
w('w2', ['r1', '2'], ['u1', 'IN+'], C_SIG),
w('w3', ['vref', 'SIG'], ['u1', 'IN-'], C_WIRE),
w('w4', ['u1', 'OUT'], ['r2', '1'], C_OUT),
w('w5', ['r2', '2'], ['u1', 'IN+'], C_SIG),
w('w6', ['vref', 'GND'], ['src', 'GND'], C_GND),
w('w7', ['u1', 'OUT'], ['vm', 'V+'], C_OUT),
w('w8', ['src', 'GND'], ['vm', 'V-'], C_GND),
],
),
];

View File

@ -5,6 +5,7 @@
*/
import { circuitExamples } from './examples-circuits';
import { analogExamples } from './examples-analog';
/** Per-board setup for multi-board examples */
export interface ExampleBoard {
@ -4802,9 +4803,14 @@ void loop() {
];
// Merge legacy examples with circuit-focused examples (analog, digital gates,
// electromechanical). Declared after both arrays exist so the export is a
// single immutable value — safe from tree-shaking quirks.
export const exampleProjects: ExampleProject[] = [...legacyExamples, ...circuitExamples];
// electromechanical) plus the board-less analog SPICE suite. Declared after
// all arrays exist so the export is a single immutable value — safe from
// tree-shaking quirks.
export const exampleProjects: ExampleProject[] = [
...legacyExamples,
...circuitExamples,
...analogExamples,
];
// Get examples by category
export function getExamplesByCategory(category: ExampleProject['category']): ExampleProject[] {

View File

@ -142,8 +142,17 @@ export async function loadExample(
recalculateAllWirePositions();
} else {
// ── Single-board loading ─────────────────────────────────────────────
const targetBoard = example.boardType || 'arduino-uno';
setBoardType(targetBoard);
// Analog-only SPICE examples are board-less. Remove every existing board
// so the canvas opens with just the analog circuit (boards are now
// optional — you can have 0, 1, or many at any time).
const isAnalogOnly = (example as any).boardFilter === 'analog';
if (isAnalogOnly) {
const currentIds = boards.map((b) => b.id);
currentIds.forEach((id) => removeBoard(id));
} else {
const targetBoard = example.boardType || 'arduino-uno';
setBoardType(targetBoard);
}
useEditorStore.getState().setCode(example.code);
const componentsWithoutBoard = example.components.filter(
@ -162,8 +171,14 @@ export async function loadExample(
})),
);
const boardInstanceId = activeBoardId ?? 'arduino-uno';
const remapBoardId = (id: string) => (isBoardComponent(id) ? boardInstanceId : id);
// After possibly removing every board, re-read activeBoardId.
const liveActiveBoardId = useSimulatorStore.getState().activeBoardId;
// For analog (board-less) examples we leave any 'arduino-uno' references
// in wires untouched — there shouldn't be any, but if there are we'd
// rather emit a dangling endpoint than silently graft them onto a board
// that no longer exists.
const remapBoardId = (id: string) =>
isBoardComponent(id) && liveActiveBoardId ? liveActiveBoardId : id;
setWires(
example.wires.map((wire) => ({