Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
9ce4ad0147
commit
eaf3fffd36
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
Loading…
Reference in New Issue