Add transistors
This commit is contained in:
@@ -70,6 +70,7 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
private const int ChipColumnSpan = 7; // 14-pin DIP
|
private const int ChipColumnSpan = 7; // 14-pin DIP
|
||||||
private const int DipSwitchColumnSpan = 8; // 16-pin DIP
|
private const int DipSwitchColumnSpan = 8; // 16-pin DIP
|
||||||
private const int PushButtonColumnSpan = 3;
|
private const int PushButtonColumnSpan = 3;
|
||||||
|
private const int TransistorColumnSpan = 3; // TO-92, three legs one column apart
|
||||||
private const int DipSwitchPositions = 8;
|
private const int DipSwitchPositions = 8;
|
||||||
|
|
||||||
private static readonly Regex UidPattern = new($@"^[A-Za-z0-9_.:-]{{1,{MaxUidLength}}}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
private static readonly Regex UidPattern = new($@"^[A-Za-z0-9_.:-]{{1,{MaxUidLength}}}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
@@ -110,7 +111,8 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
|
|
||||||
private static readonly HashSet<string> ComponentTypes = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> ComponentTypes = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"led", "resistor", "pushButton", "dipSwitch8", "powerSupply5V",
|
"led", "resistor", "diode", "npn", "pnp", "nmos", "pmos",
|
||||||
|
"pushButton", "dipSwitch8", "powerSupply5V",
|
||||||
"74HC00", "74HC02", "74HC04", "74HC08", "74HC32", "74HC86", "74HC30"
|
"74HC00", "74HC02", "74HC04", "74HC08", "74HC32", "74HC86", "74HC30"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,7 +291,21 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "led":
|
case "led":
|
||||||
ValidateLed(component, path, boardUids, errors);
|
ValidateTwoHoleSpan(component, path, boardUids, errors);
|
||||||
|
ValidateLedProps(component, path, errors);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "diode":
|
||||||
|
ValidateTwoHoleSpan(component, path, boardUids, errors);
|
||||||
|
ValidateEmptyProps(component, path, errors);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "npn":
|
||||||
|
case "pnp":
|
||||||
|
case "nmos":
|
||||||
|
case "pmos":
|
||||||
|
ValidateInlinePackage(component, path, TransistorColumnSpan, boardUids, errors);
|
||||||
|
ValidateEmptyProps(component, path, errors);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "resistor":
|
case "resistor":
|
||||||
@@ -334,15 +350,15 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An LED spans two holes: the anchor is the anode and the cathode sits one hole away in
|
/// A two-legged part spanning two holes: the anchor is the first terminal (an LED's anode,
|
||||||
/// the orient direction. A leg in a power rail is legal — electrically useless if both
|
/// a diode's anode) and the second sits one hole away in the orient direction. A leg in a
|
||||||
/// legs share a rail, but a real breadboard allows it — so only the footprint is checked.
|
/// power rail is legal — electrically useless if both legs share a rail, but a real
|
||||||
|
/// breadboard allows it — so only the footprint is checked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ValidateLed(JsonElement component, string path, HashSet<string> boardUids, ErrorList errors)
|
private static void ValidateTwoHoleSpan(JsonElement component, string path, HashSet<string> boardUids, ErrorList errors)
|
||||||
{
|
{
|
||||||
var orient = ReadEnum(component, "orient", path, Orientations, errors, required: true);
|
var orient = ReadEnum(component, "orient", path, Orientations, errors, required: true);
|
||||||
var anchor = ReadAnchor(component, path, boardUids, errors, required: true);
|
var anchor = ReadAnchor(component, path, boardUids, errors, required: true);
|
||||||
ValidateLedProps(component, path, errors);
|
|
||||||
|
|
||||||
if (anchor is null || orient is null)
|
if (anchor is null || orient is null)
|
||||||
{
|
{
|
||||||
@@ -352,7 +368,7 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
if (orient is "up" or "down")
|
if (orient is "up" or "down")
|
||||||
{
|
{
|
||||||
// Vertical travel moves one row and may legitimately cross the centre channel. A
|
// Vertical travel moves one row and may legitimately cross the centre channel. A
|
||||||
// rail has no rows, so the cathode of an up/down LED anchored there has nowhere to go.
|
// rail has no rows, so the far leg of an up/down part anchored there has nowhere to go.
|
||||||
if (anchor.Kind != "main")
|
if (anchor.Kind != "main")
|
||||||
{
|
{
|
||||||
errors.Add($"{path}.orient", "not_valid_on_rail");
|
errors.Add($"{path}.orient", "not_valid_on_rail");
|
||||||
@@ -362,8 +378,8 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
// Rows a..j read top to bottom, so "up" decreases the row index — the same sign
|
// Rows a..j read top to bottom, so "up" decreases the row index — the same sign
|
||||||
// convention that maps "left"/"right" to -1/+1 on the column axis. Mirrors
|
// convention that maps "left"/"right" to -1/+1 on the column axis. Mirrors
|
||||||
// shared/board-geometry.js (team amendment A12 pins it as contract).
|
// shared/board-geometry.js (team amendment A12 pins it as contract).
|
||||||
var cathodeRow = (anchor.Row[0] - 'a') + (orient == "up" ? -1 : 1);
|
var farRow = (anchor.Row[0] - 'a') + (orient == "up" ? -1 : 1);
|
||||||
if (cathodeRow < 0 || cathodeRow >= MainRows.Count)
|
if (farRow < 0 || farRow >= MainRows.Count)
|
||||||
{
|
{
|
||||||
errors.Add($"{path}.anchor", "footprint_off_board");
|
errors.Add($"{path}.anchor", "footprint_off_board");
|
||||||
}
|
}
|
||||||
@@ -371,16 +387,50 @@ public class BreadboardValidator(ILogger<BreadboardValidator> logger)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cathode = anchor.Position + (orient == "left" ? -1 : 1);
|
var farPosition = anchor.Position + (orient == "left" ? -1 : 1);
|
||||||
var min = anchor.Kind == "main" ? MainColumnMin : RailIndexMin;
|
var min = anchor.Kind == "main" ? MainColumnMin : RailIndexMin;
|
||||||
var max = anchor.Kind == "main" ? MainColumnMax : RailIndexMax;
|
var max = anchor.Kind == "main" ? MainColumnMax : RailIndexMax;
|
||||||
|
|
||||||
if (cathode < min || cathode > max)
|
if (farPosition < min || farPosition > max)
|
||||||
{
|
{
|
||||||
errors.Add($"{path}.anchor", "footprint_off_board");
|
errors.Add($"{path}.anchor", "footprint_off_board");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A three-legged inline package (TO-92). Its legs run along one row of the main grid, one
|
||||||
|
/// column apart, so only "left" and "right" leave each leg in a strip of its own — a
|
||||||
|
/// vertical placement would put two legs in the same five-hole strip, and a power rail is
|
||||||
|
/// one continuous strip, which is why an anchor there is rejected outright.
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateInlinePackage(
|
||||||
|
JsonElement component,
|
||||||
|
string path,
|
||||||
|
int columnSpan,
|
||||||
|
HashSet<string> boardUids,
|
||||||
|
ErrorList errors)
|
||||||
|
{
|
||||||
|
var orient = ReadEnum(component, "orient", path, PackageOrientations, errors, required: true);
|
||||||
|
var anchor = ReadAnchor(component, path, boardUids, errors, required: true);
|
||||||
|
|
||||||
|
if (anchor is null || orient is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.Kind != "main")
|
||||||
|
{
|
||||||
|
errors.Add($"{path}.anchor.kind", "must_be_main");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastColumn = anchor.Position + ((orient == "left" ? -1 : 1) * (columnSpan - 1));
|
||||||
|
if (lastColumn < MainColumnMin || lastColumn > MainColumnMax)
|
||||||
|
{
|
||||||
|
errors.Add($"{path}.anchor.col", "package_off_board");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packages that straddle the centre gap. Pin 1 sits in row "e" or "f" on the main grid
|
/// Packages that straddle the centre gap. Pin 1 sits in row "e" or "f" on the main grid
|
||||||
/// and the package runs across <paramref name="columnSpan"/> columns: row "e" reads left
|
/// and the package runs across <paramref name="columnSpan"/> columns: row "e" reads left
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
--bb-chip-pin: #c2c2c8;
|
--bb-chip-pin: #c2c2c8;
|
||||||
--bb-resistor-body: #d6c298;
|
--bb-resistor-body: #d6c298;
|
||||||
--bb-resistor-lead: #a9a9ad;
|
--bb-resistor-lead: #a9a9ad;
|
||||||
|
--bb-diode-body: #9aa7b4;
|
||||||
|
--bb-diode-band: #1c1c20;
|
||||||
|
--bb-transistor-body: #1f1f24;
|
||||||
--bb-button-body: #34343a;
|
--bb-button-body: #34343a;
|
||||||
--bb-button-cap: #c9553f;
|
--bb-button-cap: #c9553f;
|
||||||
--bb-button-cap-down: #8d3a2b;
|
--bb-button-cap-down: #8d3a2b;
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
|
|
||||||
--bb-wire-shadow: rgba(0, 0, 0, 0.22);
|
--bb-wire-shadow: rgba(0, 0, 0, 0.22);
|
||||||
--bb-chip-body: #2b2b30;
|
--bb-chip-body: #2b2b30;
|
||||||
|
--bb-transistor-body: #2b2b30;
|
||||||
--bb-supply-body: #26303a;
|
--bb-supply-body: #26303a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,95 @@ function drawResistor(ctx, component, pins, colors, state, zoom) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawDiode(ctx, component, pins, colors) {
|
||||||
|
const [anode, cathode] = pins;
|
||||||
|
if (!anode || !cathode) return;
|
||||||
|
|
||||||
|
drawLead(ctx, anode, cathode, colors);
|
||||||
|
|
||||||
|
const angle = Math.atan2(cathode.y - anode.y, cathode.x - anode.x);
|
||||||
|
const length = Math.hypot(cathode.x - anode.x, cathode.y - anode.y);
|
||||||
|
const bodyLength = Math.max(PITCH * 0.5, Math.min(length * 0.6, PITCH * 1.8));
|
||||||
|
const bodyHeight = PITCH * 0.34;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate((anode.x + cathode.x) / 2, (anode.y + cathode.y) / 2);
|
||||||
|
ctx.rotate(angle);
|
||||||
|
|
||||||
|
ctx.fillStyle = colors.diodeBody;
|
||||||
|
roundRect(ctx, -bodyLength / 2, -bodyHeight / 2, bodyLength, bodyHeight, bodyHeight * 0.3);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// The band marks the cathode, which after the rotation is always the +x end.
|
||||||
|
ctx.fillStyle = colors.diodeBand;
|
||||||
|
ctx.fillRect(bodyLength * 0.24, -bodyHeight / 2, bodyLength * 0.16, bodyHeight);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TO-92 package seen from above: a flat face with a domed back, sitting over its own
|
||||||
|
* three holes. Everything is drawn within half a pitch of the pin row so the body stays
|
||||||
|
* inside componentBounds, which is what hit-testing and dirty-rect culling use.
|
||||||
|
*/
|
||||||
|
function drawTransistor(ctx, component, pins, colors, zoom) {
|
||||||
|
const placed = pins.filter(p => p !== null);
|
||||||
|
if (placed.length < 3) return;
|
||||||
|
|
||||||
|
const xs = placed.map(p => p.x);
|
||||||
|
const ys = placed.map(p => p.y);
|
||||||
|
const x = Math.min(...xs);
|
||||||
|
const y = Math.min(...ys);
|
||||||
|
const w = Math.max(...xs) - x;
|
||||||
|
const cy = y + (Math.max(...ys) - y) / 2;
|
||||||
|
const pad = PITCH * 0.34;
|
||||||
|
const left = x - pad;
|
||||||
|
const right = x + w + pad;
|
||||||
|
const flatY = cy + PITCH * 0.42;
|
||||||
|
const backY = cy - PITCH * 0.52;
|
||||||
|
|
||||||
|
ctx.strokeStyle = colors.chipPin;
|
||||||
|
ctx.lineWidth = PITCH * 0.12;
|
||||||
|
ctx.lineCap = 'butt';
|
||||||
|
for (const pin of placed) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pin.x, flatY - PITCH * 0.1);
|
||||||
|
ctx.lineTo(pin.x, flatY + PITCH * 0.16);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = colors.transistorBody;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(left, flatY);
|
||||||
|
ctx.lineTo(right, flatY);
|
||||||
|
ctx.lineTo(right, backY + PITCH * 0.22);
|
||||||
|
ctx.quadraticCurveTo((left + right) / 2, backY - PITCH * 0.28, left, backY + PITCH * 0.22);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const def = getComponentDef(component.type);
|
||||||
|
ctx.fillStyle = colors.chipLabel;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
|
if (zoom > 1.4) {
|
||||||
|
// Leg letters, taken from the registry's pin names so they can never disagree
|
||||||
|
// with the netlist. They follow the pins, so a rotated part reads correctly.
|
||||||
|
ctx.font = `${PITCH * 0.26}px system-ui, -apple-system, "Segoe UI", sans-serif`;
|
||||||
|
ctx.textBaseline = 'bottom';
|
||||||
|
ctx.globalAlpha = 0.7;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (pins[i]) ctx.fillText(def.pins[i].name[0].toUpperCase(), pins[i].x, flatY - PITCH * 0.08);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoom > 0.9) {
|
||||||
|
ctx.font = `${PITCH * 0.3}px system-ui, -apple-system, "Segoe UI", sans-serif`;
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(def.label, (left + right) / 2, backY + PITCH * 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawPushButton(ctx, component, pins, colors, state) {
|
function drawPushButton(ctx, component, pins, colors, state) {
|
||||||
const placed = pins.filter(p => p !== null);
|
const placed = pins.filter(p => p !== null);
|
||||||
if (placed.length < 4) return;
|
if (placed.length < 4) return;
|
||||||
@@ -387,6 +476,10 @@ export function drawComponent(ctx, component, boards, colors, state, zoom) {
|
|||||||
switch (component.type) {
|
switch (component.type) {
|
||||||
case 'led': drawLed(ctx, component, pins, colors, state); break;
|
case 'led': drawLed(ctx, component, pins, colors, state); break;
|
||||||
case 'resistor': drawResistor(ctx, component, pins, colors, state, zoom); break;
|
case 'resistor': drawResistor(ctx, component, pins, colors, state, zoom); break;
|
||||||
|
case 'diode': drawDiode(ctx, component, pins, colors); break;
|
||||||
|
case 'npn': case 'pnp': case 'nmos': case 'pmos':
|
||||||
|
drawTransistor(ctx, component, pins, colors, zoom);
|
||||||
|
break;
|
||||||
case 'pushButton': drawPushButton(ctx, component, pins, colors, state); break;
|
case 'pushButton': drawPushButton(ctx, component, pins, colors, state); break;
|
||||||
case 'dipSwitch8': drawDipSwitch(ctx, component, pins, colors, state, zoom); break;
|
case 'dipSwitch8': drawDipSwitch(ctx, component, pins, colors, state, zoom); break;
|
||||||
case 'powerSupply5V': drawPowerSupply(ctx, component, pins, colors, state, zoom, boards); break;
|
case 'powerSupply5V': drawPowerSupply(ctx, component, pins, colors, state, zoom, boards); break;
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { el, button, createListenerBag } from './dom.js';
|
|||||||
import { COMPONENT_TYPES, getComponentDef } from '../shared/component-registry.js';
|
import { COMPONENT_TYPES, getComponentDef } from '../shared/component-registry.js';
|
||||||
import { WIRE_COLORS, DEFAULT_WIRE_COLOR } from '../shared/circuit-schema.js';
|
import { WIRE_COLORS, DEFAULT_WIRE_COLOR } from '../shared/circuit-schema.js';
|
||||||
|
|
||||||
const CATEGORY_ORDER = Object.freeze(['passive', 'output', 'input', 'power', 'chip']);
|
const CATEGORY_ORDER = Object.freeze(['passive', 'semiconductor', 'output', 'input', 'power', 'chip']);
|
||||||
const CATEGORY_LABELS = Object.freeze(Object.assign(Object.create(null), {
|
const CATEGORY_LABELS = Object.freeze(Object.assign(Object.create(null), {
|
||||||
passive: 'Passive',
|
passive: 'Passive',
|
||||||
|
semiconductor: 'Semiconductor',
|
||||||
output: 'Output',
|
output: 'Output',
|
||||||
input: 'Input',
|
input: 'Input',
|
||||||
power: 'Power',
|
power: 'Power',
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const WARNING_LABELS = Object.freeze(Object.assign(Object.create(null), {
|
|||||||
unknownComponent: 'Unrecognised component',
|
unknownComponent: 'Unrecognised component',
|
||||||
unconnectedSupply: 'Supply is not connected to anything',
|
unconnectedSupply: 'Supply is not connected to anything',
|
||||||
selfShorted: 'Both ends are on the same net',
|
selfShorted: 'Both ends are on the same net',
|
||||||
|
floatingControl: 'Nothing is connected to the base or gate',
|
||||||
|
unlimitedBaseCurrent: 'Transistor base has no series resistor',
|
||||||
engineError: 'The simulation engine hit an internal error',
|
engineError: 'The simulation engine hit an internal error',
|
||||||
warningsSuppressed: 'Some warnings were coalesced'
|
warningsSuppressed: 'Some warnings were coalesced'
|
||||||
}));
|
}));
|
||||||
@@ -39,7 +41,8 @@ const SEVERITY = Object.freeze(Object.assign(Object.create(null), {
|
|||||||
shortCircuit: 'error',
|
shortCircuit: 'error',
|
||||||
contention: 'error',
|
contention: 'error',
|
||||||
oscillation: 'warn',
|
oscillation: 'warn',
|
||||||
ledOvercurrent: 'warn'
|
ledOvercurrent: 'warn',
|
||||||
|
unlimitedBaseCurrent: 'warn'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function humanizeKind(kind) {
|
function humanizeKind(kind) {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const TOKENS = Object.freeze({
|
|||||||
chipPin: ['--bb-chip-pin', '#c8c8cc'],
|
chipPin: ['--bb-chip-pin', '#c8c8cc'],
|
||||||
resistorBody: ['--bb-resistor-body', '#d8c49a'],
|
resistorBody: ['--bb-resistor-body', '#d8c49a'],
|
||||||
resistorLead: ['--bb-resistor-lead', '#b0b0b0'],
|
resistorLead: ['--bb-resistor-lead', '#b0b0b0'],
|
||||||
|
diodeBody: ['--bb-diode-body', '#9aa7b4'],
|
||||||
|
diodeBand: ['--bb-diode-band', '#1c1c20'],
|
||||||
|
transistorBody: ['--bb-transistor-body', '#1f1f24'],
|
||||||
buttonBody: ['--bb-button-body', '#3a3a3e'],
|
buttonBody: ['--bb-button-body', '#3a3a3e'],
|
||||||
buttonCap: ['--bb-button-cap', '#c9553f'],
|
buttonCap: ['--bb-button-cap', '#c9553f'],
|
||||||
buttonCapDown: ['--bb-button-cap-down', '#8d3a2b'],
|
buttonCapDown: ['--bb-button-cap-down', '#8d3a2b'],
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Bidirectional conduction between two pins of one component.
|
||||||
|
*
|
||||||
|
* Shared by every element that opens and closes a channel: mechanical contacts
|
||||||
|
* (push button, DIP switch) and the transistors. Conduction is NOT a dynamic
|
||||||
|
* merge of the two nets — re-running union-find every time a contact moves
|
||||||
|
* would be slow, and it would invalidate the net index the UI is handed once at
|
||||||
|
* load. Each side simply drives the other with what it sees.
|
||||||
|
*
|
||||||
|
* Conduction preserves STRENGTH, unlike a resistor: a closed contact between
|
||||||
|
* the two power rails has to hand SUPPLY strength across so the short reads as
|
||||||
|
* a short circuit and not as garden-variety contention. The same is true of a
|
||||||
|
* saturated transistor, which is why it destroys itself in real life.
|
||||||
|
*
|
||||||
|
* The source net is always read EXCLUDING the element's own contribution to it,
|
||||||
|
* or a closed channel would read back the value it is itself asserting and
|
||||||
|
* latch onto it forever after the real source went away.
|
||||||
|
*
|
||||||
|
* Every caller passes a non-zero delay. It is a loop breaker rather than a
|
||||||
|
* physical figure: two elements wired in a ring would otherwise conduct round
|
||||||
|
* it at zero delay forever and trip the delta-cycle guard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { STRENGTH_HIGHZ, VALUE_LOW } from '../constants.js';
|
||||||
|
import { driveStrength, driveMask, MASK_LOW, MASK_HIGH } from '../drive.js';
|
||||||
|
|
||||||
|
/** Passes `fromPin`'s net through to `toPin` at full strength, or opens the channel. */
|
||||||
|
export function conduct(ctx, inst, fromPin, toPin, closed, delayNs) {
|
||||||
|
if (!closed) {
|
||||||
|
ctx.drive(inst, toPin, STRENGTH_HIGHZ, VALUE_LOW, delayNs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const drive = ctx.driveExcludingSelf(inst, fromPin);
|
||||||
|
const strength = driveStrength(drive);
|
||||||
|
const mask = driveMask(drive);
|
||||||
|
// A source that is itself unresolved (both polarities present) passes
|
||||||
|
// nothing: there is no single value to conduct.
|
||||||
|
if (strength === STRENGTH_HIGHZ || (mask !== MASK_LOW && mask !== MASK_HIGH)) {
|
||||||
|
ctx.drive(inst, toPin, STRENGTH_HIGHZ, VALUE_LOW, delayNs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.drive(inst, toPin, strength, mask === MASK_HIGH ? 1 : 0, delayNs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refreshes both directions of the bridge between `a` and `b`. */
|
||||||
|
export function refreshBridge(ctx, inst, a, b, closed, delayNs) {
|
||||||
|
conduct(ctx, inst, a, b, closed, delayNs);
|
||||||
|
conduct(ctx, inst, b, a, closed, delayNs);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Diode. Pin 1 is the anode, pin 2 the banded cathode.
|
||||||
|
*
|
||||||
|
* A one-way conductor: forward biased it passes the anode's drive through to
|
||||||
|
* the cathode at full strength (conduction.js), reverse biased it passes
|
||||||
|
* nothing, and it NEVER drives its anode in either state. That asymmetry is the
|
||||||
|
* whole part — it is what makes diode-OR steering, polarity protection and a
|
||||||
|
* freewheel path across an inductive load behave differently from a wire.
|
||||||
|
*
|
||||||
|
* Forward bias is decided from the cathode read EXCLUDING this diode's own
|
||||||
|
* contribution, for the same reason the transistors do it: a diode charging an
|
||||||
|
* otherwise-floating node would otherwise see the node it had just pulled high,
|
||||||
|
* conclude it was no longer forward biased, release, and oscillate.
|
||||||
|
*
|
||||||
|
* The cathode is treated as blocking whenever it is ALREADY high — not only
|
||||||
|
* when it is driven high by something stronger. Two supplies steered into one
|
||||||
|
* node through a diode each is the ordinary case, and neither diode should
|
||||||
|
* report conducting into the other's output.
|
||||||
|
*
|
||||||
|
* LIMIT OF THIS MODEL. There is no forward voltage drop, because there is no
|
||||||
|
* voltage between 0 V and 5 V to drop it to (see resistor.js). A diode here is
|
||||||
|
* a switch that only closes one way; a chain of them does not stack up 0.7 V a
|
||||||
|
* time, and a diode cannot be used as a voltage reference. Reverse breakdown is
|
||||||
|
* not modelled either, so a Zener cannot be built from one. Forward voltage
|
||||||
|
* DOES matter for an LED, and lives in the LED's own model.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LEVEL_HIGH, LEVEL_WEAK_HIGH } from '../constants.js';
|
||||||
|
import { defineModel } from './registry.js';
|
||||||
|
import { conduct } from './conduction.js';
|
||||||
|
|
||||||
|
/** Loop breaker, as everywhere else conduction is instantaneous in reality. */
|
||||||
|
const DIODE_DELAY_NS = 1;
|
||||||
|
|
||||||
|
const PIN_ANODE = 0;
|
||||||
|
const PIN_CATHODE = 1;
|
||||||
|
|
||||||
|
function isHigh(level) {
|
||||||
|
return level === LEVEL_HIGH || level === LEVEL_WEAK_HIGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshBias(ctx, inst) {
|
||||||
|
// The anode needs no exclusion: a diode never drives its own anode, so its
|
||||||
|
// driver there is high-Z and contributes nothing to exclude.
|
||||||
|
const forward = isHigh(ctx.level(inst, PIN_ANODE)) && !isHigh(ctx.levelExcludingSelf(inst, PIN_CATHODE));
|
||||||
|
conduct(ctx, inst, PIN_ANODE, PIN_CATHODE, forward, DIODE_DELAY_NS);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineModel({
|
||||||
|
type: 'diode',
|
||||||
|
pinCount: 2,
|
||||||
|
delayNs: DIODE_DELAY_NS,
|
||||||
|
functionalPins: [[1, 2]],
|
||||||
|
|
||||||
|
init(ctx, inst) {
|
||||||
|
refreshBias(ctx, inst);
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluate(ctx, inst) {
|
||||||
|
refreshBias(ctx, inst);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -11,5 +11,7 @@ import './supply.js';
|
|||||||
import './resistor.js';
|
import './resistor.js';
|
||||||
import './switches.js';
|
import './switches.js';
|
||||||
import './led.js';
|
import './led.js';
|
||||||
|
import './diode.js';
|
||||||
|
import './transistor.js';
|
||||||
|
|
||||||
export { registry, getModel, knownTypes, defineModel, WAKE_INIT, WAKE_PIN, WAKE_TIMER } from './registry.js';
|
export { registry, getModel, knownTypes, defineModel, WAKE_INIT, WAKE_PIN, WAKE_TIMER } from './registry.js';
|
||||||
|
|||||||
@@ -1,50 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Mechanical contacts: push button and 8-position DIP switch.
|
* Mechanical contacts: push button and 8-position DIP switch.
|
||||||
*
|
*
|
||||||
* A closed contact is modelled as a bidirectional conductor rather than a
|
* Both are pure conduction elements — see conduction.js for why a closed
|
||||||
* dynamic merge of the two nets. Re-running union-find every time a user
|
* contact drives across rather than merging the two nets, and why it preserves
|
||||||
* presses a button would be both slow and a nightmare for the UI's net index,
|
* drive strength while doing so.
|
||||||
* which is handed out once at load and must stay valid.
|
|
||||||
*
|
|
||||||
* Conduction preserves STRENGTH, unlike a resistor: a closed button between the
|
|
||||||
* two power rails has to hand SUPPLY strength across so the short reads as a
|
|
||||||
* short circuit and not as garden-variety contention.
|
|
||||||
*
|
*
|
||||||
* The 1 ns contact delay is a loop breaker, not a debounce model. Two contacts
|
* The 1 ns contact delay is a loop breaker, not a debounce model. Two contacts
|
||||||
* wired in a ring would otherwise conduct round it at zero delay forever.
|
* wired in a ring would otherwise conduct round it at zero delay forever.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { STRENGTH_HIGHZ, VALUE_LOW } from '../constants.js';
|
|
||||||
import { driveStrength, driveMask, MASK_LOW, MASK_HIGH } from '../drive.js';
|
|
||||||
import { defineModel, WAKE_PIN } from './registry.js';
|
import { defineModel, WAKE_PIN } from './registry.js';
|
||||||
|
import { refreshBridge } from './conduction.js';
|
||||||
|
|
||||||
const CONTACT_DELAY_NS = 1;
|
const CONTACT_DELAY_NS = 1;
|
||||||
|
|
||||||
/**
|
/** Closes or opens the contact bridging `a` and `b`. */
|
||||||
* Passes `fromPin`'s net through to `toPin` at full strength, or opens the
|
function setContact(ctx, inst, a, b, closed) {
|
||||||
* contact. Reads the source net excluding this element's own contribution, so
|
refreshBridge(ctx, inst, a, b, closed, CONTACT_DELAY_NS);
|
||||||
* the contact cannot latch onto the value it is itself asserting.
|
|
||||||
*/
|
|
||||||
function conduct(ctx, inst, fromPin, toPin, closed) {
|
|
||||||
if (!closed) {
|
|
||||||
ctx.drive(inst, toPin, STRENGTH_HIGHZ, VALUE_LOW, CONTACT_DELAY_NS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const drive = ctx.driveExcludingSelf(inst, fromPin);
|
|
||||||
const strength = driveStrength(drive);
|
|
||||||
const mask = driveMask(drive);
|
|
||||||
// A source that is itself unresolved (both polarities present) passes
|
|
||||||
// nothing: there is no single value to conduct.
|
|
||||||
if (strength === STRENGTH_HIGHZ || (mask !== MASK_LOW && mask !== MASK_HIGH)) {
|
|
||||||
ctx.drive(inst, toPin, STRENGTH_HIGHZ, VALUE_LOW, CONTACT_DELAY_NS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.drive(inst, toPin, strength, mask === MASK_HIGH ? 1 : 0, CONTACT_DELAY_NS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshContact(ctx, inst, a, b, closed) {
|
|
||||||
conduct(ctx, inst, a, b, closed);
|
|
||||||
conduct(ctx, inst, b, a, closed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ------------------------------------------------------------------ *
|
||||||
@@ -70,12 +42,12 @@ defineModel({
|
|||||||
},
|
},
|
||||||
|
|
||||||
init(ctx, inst) {
|
init(ctx, inst) {
|
||||||
refreshContact(ctx, inst, BUTTON_A, BUTTON_B, inst.state.pressed);
|
setContact(ctx, inst, BUTTON_A, BUTTON_B, inst.state.pressed);
|
||||||
},
|
},
|
||||||
|
|
||||||
evaluate(ctx, inst, wake) {
|
evaluate(ctx, inst, wake) {
|
||||||
if (wake.reason === WAKE_PIN && wake.pin !== BUTTON_A && wake.pin !== BUTTON_B) return;
|
if (wake.reason === WAKE_PIN && wake.pin !== BUTTON_A && wake.pin !== BUTTON_B) return;
|
||||||
refreshContact(ctx, inst, BUTTON_A, BUTTON_B, inst.state.pressed);
|
setContact(ctx, inst, BUTTON_A, BUTTON_B, inst.state.pressed);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** `{ type:"input", uid, value }` — value is a boolean: pressed or released. */
|
/** `{ type:"input", uid, value }` — value is a boolean: pressed or released. */
|
||||||
@@ -83,7 +55,7 @@ defineModel({
|
|||||||
const pressed = value === true || value?.pressed === true;
|
const pressed = value === true || value?.pressed === true;
|
||||||
if (inst.state.pressed === pressed) return;
|
if (inst.state.pressed === pressed) return;
|
||||||
inst.state.pressed = pressed;
|
inst.state.pressed = pressed;
|
||||||
refreshContact(ctx, inst, BUTTON_A, BUTTON_B, pressed);
|
setContact(ctx, inst, BUTTON_A, BUTTON_B, pressed);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +88,7 @@ defineModel({
|
|||||||
init(ctx, inst) {
|
init(ctx, inst) {
|
||||||
for (let k = 1; k <= DIP_SWITCH_COUNT; k++) {
|
for (let k = 1; k <= DIP_SWITCH_COUNT; k++) {
|
||||||
const [a, b] = dipPins(k);
|
const [a, b] = dipPins(k);
|
||||||
refreshContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
setContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -129,7 +101,7 @@ defineModel({
|
|||||||
if (wake.reason !== WAKE_PIN) {
|
if (wake.reason !== WAKE_PIN) {
|
||||||
for (let k = 1; k <= DIP_SWITCH_COUNT; k++) {
|
for (let k = 1; k <= DIP_SWITCH_COUNT; k++) {
|
||||||
const [a, b] = dipPins(k);
|
const [a, b] = dipPins(k);
|
||||||
refreshContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
setContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,7 +110,7 @@ defineModel({
|
|||||||
if (!(pin >= 0) || pin > 15) return;
|
if (!(pin >= 0) || pin > 15) return;
|
||||||
const k = pin < DIP_SWITCH_COUNT ? pin + 1 : 16 - pin;
|
const k = pin < DIP_SWITCH_COUNT ? pin + 1 : 16 - pin;
|
||||||
const [a, b] = dipPins(k);
|
const [a, b] = dipPins(k);
|
||||||
refreshContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
setContact(ctx, inst, a, b, inst.state.on[k - 1] === 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,6 +124,6 @@ defineModel({
|
|||||||
if (inst.state.on[k - 1] === on) return;
|
if (inst.state.on[k - 1] === on) return;
|
||||||
inst.state.on[k - 1] = on;
|
inst.state.on[k - 1] = on;
|
||||||
const [a, b] = dipPins(k);
|
const [a, b] = dipPins(k);
|
||||||
refreshContact(ctx, inst, a, b, on === 1);
|
setContact(ctx, inst, a, b, on === 1);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Transistors: NPN and PNP bipolars, N- and P-channel MOSFETs.
|
||||||
|
*
|
||||||
|
* All four are the same device to this engine — a channel between pin 1 and
|
||||||
|
* pin 3 that the control pin on pin 2 opens and closes — so they share one
|
||||||
|
* definition factory and differ only in the polarity of the turn-on test and in
|
||||||
|
* which mistakes are worth warning about. Pin order is the physical TO-92 one,
|
||||||
|
* control terminal in the middle: emitter/base/collector, source/gate/drain.
|
||||||
|
*
|
||||||
|
* Turning on takes BOTH terminals into account, never the control pin alone. An
|
||||||
|
* NPN conducts when its base is above its emitter, so an NPN whose emitter is
|
||||||
|
* already sitting on the 5 V rail stays off no matter what its base does — the
|
||||||
|
* single most common reason a beginner's high-side switch does nothing.
|
||||||
|
*
|
||||||
|
* The channel terminal is read EXCLUDING this device's own contribution, and
|
||||||
|
* that is load-bearing rather than defensive. An emitter follower pulls its own
|
||||||
|
* emitter up to what it is switching; read plainly, the device would then see
|
||||||
|
* base and emitter at the same level, decide Vbe had collapsed, turn off, drop
|
||||||
|
* the emitter, turn on again, and oscillate forever at the switching delay. The
|
||||||
|
* exclusion asks the question that actually decides conduction: would current
|
||||||
|
* flow if this device were not already conducting?
|
||||||
|
*
|
||||||
|
* WHAT THIS MODEL IS NOT. There is no linear region and no gain: the device is
|
||||||
|
* saturated or cut off, nothing between. There is no Vbe and no Vce(sat), so a
|
||||||
|
* follower's output does not sit 0.7 V below its base — the engine has no
|
||||||
|
* voltage between 0 and 5 V to put it at (see resistor.js). And the control pin
|
||||||
|
* is a pure high-Z input drawing no base current, which is exactly why the
|
||||||
|
* unlimited-base warning below has to exist: the model cannot punish a missing
|
||||||
|
* base resistor by melting, so it says so instead. A MOSFET's intrinsic body
|
||||||
|
* diode is not modelled either, so an off device never conducts backwards.
|
||||||
|
*
|
||||||
|
* A conducting device passes drive strength through unchanged (conduction.js),
|
||||||
|
* so a transistor wired collector-to-rail and emitter-to-ground still reports
|
||||||
|
* the short circuit it would really be.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LEVEL_HIGH, LEVEL_WEAK_HIGH, LEVEL_LOW, LEVEL_WEAK_LOW, NO_NET, STRENGTH_SUPPLY } from '../constants.js';
|
||||||
|
import { driveStrength } from '../drive.js';
|
||||||
|
import { defineModel } from './registry.js';
|
||||||
|
import { refreshBridge } from './conduction.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switching delay, ns. Slower than a contact bridging two nets because a real
|
||||||
|
* transistor's storage and rise time genuinely dominate it, and comfortably
|
||||||
|
* non-zero so a discrete inverter ring oscillates at a rate rather than
|
||||||
|
* tripping the delta-cycle guard.
|
||||||
|
*/
|
||||||
|
const SWITCHING_DELAY_NS = 10;
|
||||||
|
|
||||||
|
const PIN_CHANNEL_LOW = 0; // emitter / source
|
||||||
|
const PIN_CONTROL = 1; // base / gate
|
||||||
|
const PIN_CHANNEL_HIGH = 2; // collector / drain
|
||||||
|
|
||||||
|
function isHigh(level) {
|
||||||
|
return level === LEVEL_HIGH || level === LEVEL_WEAK_HIGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLow(level) {
|
||||||
|
return level === LEVEL_LOW || level === LEVEL_WEAK_LOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the channel is open. A floating control pin reads as neither high nor
|
||||||
|
* low and therefore leaves the device off, which is the right answer for a
|
||||||
|
* bipolar (no base current, no conduction) and the safe one for a MOSFET, whose
|
||||||
|
* real gate would drift somewhere unpredictable — hence the load-time warning.
|
||||||
|
*/
|
||||||
|
function isConducting(ctx, inst) {
|
||||||
|
const control = ctx.level(inst, PIN_CONTROL);
|
||||||
|
const channel = ctx.levelExcludingSelf(inst, PIN_CHANNEL_LOW);
|
||||||
|
return inst.model.pChannel ? isLow(control) && isHigh(channel) : isHigh(control) && isLow(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshChannel(ctx, inst) {
|
||||||
|
const conducting = isConducting(ctx, inst);
|
||||||
|
refreshBridge(ctx, inst, PIN_CHANNEL_LOW, PIN_CHANNEL_HIGH, conducting, SWITCHING_DELAY_NS);
|
||||||
|
return conducting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A control pin nothing else touches can never switch the device. */
|
||||||
|
function warnIfControlFloating(ctx, inst) {
|
||||||
|
const net = ctx.netOf(inst, PIN_CONTROL);
|
||||||
|
if (net !== NO_NET && ctx.netListenerCount(net) > 1) return;
|
||||||
|
ctx.staticWarning(
|
||||||
|
'floatingControl',
|
||||||
|
`${inst.type} ${inst.uid}: nothing is connected to its ${inst.model.controlName}, so it can never switch on`,
|
||||||
|
[inst.uid],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base current is what destroys a bipolar driven straight from a logic output,
|
||||||
|
* and this engine draws none — so the mistake is reported the first time the
|
||||||
|
* device actually conducts with an unlimited base rather than at load, where a
|
||||||
|
* base deliberately tied to ground to hold the part off would trip it too.
|
||||||
|
* Latched per instance: a transistor switching at 1 MHz must not re-warn.
|
||||||
|
*
|
||||||
|
* Evidence of a base resistor is a resistor touching the base's net, which is
|
||||||
|
* the same dominant-resistance approximation the LED uses and inherits the same
|
||||||
|
* blind spot — a resistor on that net need not be in series with the base. The
|
||||||
|
* one place that approximation is not merely imprecise but WRONG is a base
|
||||||
|
* clipped straight onto a power rail: every part on the board shares its rails,
|
||||||
|
* so a rail nearly always carries some resistor, yet nothing is in series with
|
||||||
|
* a base sitting on it. Supply drive strength is exactly what identifies that
|
||||||
|
* case, so it is tested first and overrides the resistor evidence.
|
||||||
|
*
|
||||||
|
* The result errs one way only: it can stay quiet about a real mistake, and
|
||||||
|
* never invents one.
|
||||||
|
*/
|
||||||
|
function warnIfBaseUnlimited(ctx, inst) {
|
||||||
|
if (inst.state.warnedBaseDrive) return;
|
||||||
|
const net = ctx.netOf(inst, PIN_CONTROL);
|
||||||
|
if (net === NO_NET) return;
|
||||||
|
const onSupplyRail = driveStrength(ctx.driveExcludingSelf(inst, PIN_CONTROL)) === STRENGTH_SUPPLY;
|
||||||
|
if (!onSupplyRail && ctx.dominantSeriesOhms(net) > 0) return;
|
||||||
|
inst.state.warnedBaseDrive = true;
|
||||||
|
ctx.warn(
|
||||||
|
'unlimitedBaseCurrent',
|
||||||
|
[inst.uid],
|
||||||
|
net,
|
||||||
|
`${inst.type} ${inst.uid} is switched on through a base with no series resistor; a real one would draw destructive base current`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineTransistor(definition) {
|
||||||
|
defineModel({
|
||||||
|
pinCount: 3,
|
||||||
|
delayNs: SWITCHING_DELAY_NS,
|
||||||
|
// The two channel terminals must reach different nets for the device to
|
||||||
|
// switch anything; the control pin may legitimately share a net with
|
||||||
|
// either (a gate tied to its source is simply held off).
|
||||||
|
functionalPins: [[1, 3]],
|
||||||
|
|
||||||
|
createState() {
|
||||||
|
return { warnedBaseDrive: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
init(ctx, inst) {
|
||||||
|
warnIfControlFloating(ctx, inst);
|
||||||
|
refreshChannel(ctx, inst);
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluate(ctx, inst) {
|
||||||
|
// Any pin can change the answer: the control pin decides drive, the
|
||||||
|
// channel-low pin decides whether there is a potential to drive it
|
||||||
|
// with, and the channel-high pin changes what gets passed across.
|
||||||
|
const conducting = refreshChannel(ctx, inst);
|
||||||
|
if (conducting && inst.model.bipolar) warnIfBaseUnlimited(ctx, inst);
|
||||||
|
},
|
||||||
|
|
||||||
|
...definition,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineTransistor({ type: 'npn', pChannel: false, bipolar: true, controlName: 'base' });
|
||||||
|
defineTransistor({ type: 'pnp', pChannel: true, bipolar: true, controlName: 'base' });
|
||||||
|
defineTransistor({ type: 'nmos', pChannel: false, bipolar: false, controlName: 'gate' });
|
||||||
|
defineTransistor({ type: 'pmos', pChannel: true, bipolar: false, controlName: 'gate' });
|
||||||
@@ -116,6 +116,52 @@ function chipDef(type) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orientations a three-legged inline package may carry. Its legs run along a row, so
|
||||||
|
* only the two horizontal directions leave each leg in a strip of its own: a column's
|
||||||
|
* rows a-e are one strip, which would short two legs of any vertical placement.
|
||||||
|
*/
|
||||||
|
export const INLINE_ORIENTATIONS = Object.freeze(['left', 'right']);
|
||||||
|
|
||||||
|
/** Footprint for a part whose pins step away from the anchor in the orient direction. */
|
||||||
|
function orientStepFootprint(pinCount) {
|
||||||
|
const pins = [];
|
||||||
|
for (let pin = 1; pin <= pinCount; pin++) {
|
||||||
|
pins.push(pin === 1
|
||||||
|
? Object.freeze({ pin, at: 'anchor' })
|
||||||
|
: Object.freeze({ pin, at: 'orientStep', steps: pin - 1 }));
|
||||||
|
}
|
||||||
|
return Object.freeze({ kind: 'orientStep', pinCount, pins: Object.freeze(pins) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The four transistors differ only in polarity and in what their legs are called; the
|
||||||
|
* package, the footprint and the placement rules are one part. Pin order is the
|
||||||
|
* physical TO-92 one, control terminal in the middle.
|
||||||
|
*/
|
||||||
|
function transistorDef(type, label, description, pinNames) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
category: 'semiconductor',
|
||||||
|
pins: namedPins(pinNames),
|
||||||
|
bodyColumns: 3,
|
||||||
|
straddlesGap: false,
|
||||||
|
anchorRows: null,
|
||||||
|
// A power rail is one continuous strip, so all three legs there would be common.
|
||||||
|
anchorKinds: Object.freeze(['main']),
|
||||||
|
anchorless: false,
|
||||||
|
orientable: true,
|
||||||
|
orientValues: INLINE_ORIENTATIONS,
|
||||||
|
defaultOrient: 'right',
|
||||||
|
dipStyle: false,
|
||||||
|
footprint: orientStepFootprint(3),
|
||||||
|
defaultProps: Object.freeze({}),
|
||||||
|
propSpecs: Object.freeze({})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const DEFS = Object.create(null);
|
const DEFS = Object.create(null);
|
||||||
|
|
||||||
function define(def) {
|
function define(def) {
|
||||||
@@ -186,6 +232,45 @@ define({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
define({
|
||||||
|
type: 'diode',
|
||||||
|
label: 'Diode',
|
||||||
|
description: 'Signal diode (1N4148). Anchor is the anode; the banded cathode sits one hole away. Passes current one way only.',
|
||||||
|
category: 'semiconductor',
|
||||||
|
pins: namedPins(['anode', 'cathode']),
|
||||||
|
bodyColumns: 1,
|
||||||
|
straddlesGap: false,
|
||||||
|
anchorRows: null,
|
||||||
|
anchorKinds: Object.freeze(['main', 'rail']),
|
||||||
|
anchorless: false,
|
||||||
|
orientable: true,
|
||||||
|
orientValues: Object.freeze(['up', 'down', 'left', 'right']),
|
||||||
|
// Same reasoning as the LED: 'right' is the only default that reaches a different
|
||||||
|
// strip from every main column and also works from a rail hole.
|
||||||
|
defaultOrient: 'right',
|
||||||
|
dipStyle: false,
|
||||||
|
footprint: orientStepFootprint(2),
|
||||||
|
defaultProps: Object.freeze({}),
|
||||||
|
propSpecs: Object.freeze({})
|
||||||
|
});
|
||||||
|
|
||||||
|
const TRANSISTOR_DEFS = Object.freeze([
|
||||||
|
transistorDef('npn', 'NPN',
|
||||||
|
'NPN transistor (2N3904): emitter, base, collector. Conducts when the base is high and the emitter is the low side, so it switches a load to ground.',
|
||||||
|
['emitter', 'base', 'collector']),
|
||||||
|
transistorDef('pnp', 'PNP',
|
||||||
|
'PNP transistor (2N3906): emitter, base, collector. Conducts when the base is low and the emitter is the high side, so it switches a load to the supply.',
|
||||||
|
['emitter', 'base', 'collector']),
|
||||||
|
transistorDef('nmos', 'N-MOSFET',
|
||||||
|
'N-channel MOSFET (2N7000): source, gate, drain. Conducts when the gate is high and the source is the low side. The gate draws no current, so it needs a pull-down to stay off.',
|
||||||
|
['source', 'gate', 'drain']),
|
||||||
|
transistorDef('pmos', 'P-MOSFET',
|
||||||
|
'P-channel MOSFET (BS250): source, gate, drain. Conducts when the gate is low and the source is the high side. The gate draws no current, so it needs a pull-up to stay off.',
|
||||||
|
['source', 'gate', 'drain'])
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const def of TRANSISTOR_DEFS) define(def);
|
||||||
|
|
||||||
define({
|
define({
|
||||||
type: 'pushButton',
|
type: 'pushButton',
|
||||||
label: 'Push button',
|
label: 'Push button',
|
||||||
@@ -277,7 +362,8 @@ Object.freeze(DEFS);
|
|||||||
|
|
||||||
/** All registered component type names, in palette order. */
|
/** All registered component type names, in palette order. */
|
||||||
export const COMPONENT_TYPES = Object.freeze([
|
export const COMPONENT_TYPES = Object.freeze([
|
||||||
'led', 'resistor', 'pushButton', 'dipSwitch8', 'powerSupply5V', ...CHIP_TYPES
|
'led', 'resistor', 'diode', ...TRANSISTOR_DEFS.map(d => d.type),
|
||||||
|
'pushButton', 'dipSwitch8', 'powerSupply5V', ...CHIP_TYPES
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** Valid `orient` values for orientable components. */
|
/** Valid `orient` values for orientable components. */
|
||||||
|
|||||||
Reference in New Issue
Block a user