feat: breadboard (full 830-point) + mini breadboard (170-point) parts

Two velxio-native passive parts, rendered as web components with
programmatic SVG + precomputed pinInfo (velxio-breadboard 830 holes,
velxio-breadboard-mini 170). Pin names follow the Wokwi convention
(holes `18t.d` / `17b.i`, rails `tp/tn/bp/bn.N`) and the metadata ids
are `breadboard` / `breadboard-mini`, so wokwi diagram.json zips
import/export with no aliasing.

Internal connectivity (5-hole column strips, full-length power rails)
is centralized in utils/breadboardNets.ts and wired into every net
consumer:
- NetlistBuilder: unionBreadboardGroups joins wired holes per group at
  the union-find level in buildNetlist, buildWireNetMap and
  buildBoardPinNetMap — SPICE, the circuit verifier and the voltage
  overlay all see one net per strip/rail with no extra cards.
- DynamicComponent.traceDetailed: the digital trace hops through every
  other wired hole of the entered group, so parts wired through a
  breadboard still resolve their board pin (2-terminal
  PASSIVE_PIN_PAIRS could not express N-hole groups).

Verified end-to-end in the app: Uno pin 8 -> full-board column ->
resistor -> mini-board column -> LED -> ground rail -> GND lights the
LED, and the HUD shows the 3 collapsed SPICE nets. 8 new unit tests
(breadboard-nets.test.ts); netlist-builder + circuit-verifier suites
stay green.
This commit is contained in:
David Montero Crespo 2026-07-15 21:50:13 +02:00
parent 814755b249
commit 6307aed007
9 changed files with 583 additions and 29 deletions

View File

@ -1450,7 +1450,7 @@
"defaultValues": {
"i2cAddress": "0x3c"
},
"pinCount": 8,
"pinCount": 0,
"tags": [
"ssd1306",
"oled",
@ -1460,6 +1460,35 @@
"0x3c"
]
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#025caf\" rx=\"6\"/><rect x=\"9\" y=\"24\" width=\"46\" height=\"24\" fill=\"#141414\"/><text x=\"32\" y=\"14\" text-anchor=\"middle\" font-size=\"5\" font-family=\"monospace\" fill=\"#fff\">GND VCC SCL SDA</text><circle cx=\"18\" cy=\"19\" r=\"1.6\" fill=\"#b4aeab\"/><circle cx=\"27\" cy=\"19\" r=\"1.6\" fill=\"#c08540\"/><circle cx=\"37\" cy=\"19\" r=\"1.6\" fill=\"#009e9b\"/><circle cx=\"46\" cy=\"19\" r=\"1.6\" fill=\"#007adb\"/><text x=\"32\" y=\"39\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"46\" text-anchor=\"middle\" font-size=\"4.5\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 4-pin</text></svg>",
"tags": [
"ssd1306",
"oled",
"display",
"i2c",
"4-pin",
"0x3c"
],
"properties": [
{
"name": "i2cAddress",
"type": "string",
"defaultValue": "0x3c",
"control": "text",
"description": "I2C address"
}
],
"defaultValues": {
"i2cAddress": "0x3c"
},
"pinCount": 4,
"$comment": "4-pin I2C-only SSD1306 module (GND/VCC/SCL/SDA) — the cheap 0.96\" OLED most beginners have; velxio-ssd1306-i2c native element. Counterpart to the 8-pin wokwi-ssd1306. Issue #215.",
"id": "ssd1306-i2c-4pin",
"tagName": "velxio-ssd1306-i2c-4pin",
"name": "SSD1306 OLED (I2C, 4-pin)",
"category": "displays"
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n MOTOR-DRIVER-L293D\n </text>\n </svg>",
"tags": [
@ -3325,6 +3354,44 @@
"led"
]
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"14\" width=\"60\" height=\"36\" rx=\"3\" fill=\"#f4f1ec\" stroke=\"#c8c4bc\"/><line x1=\"5\" y1=\"19\" x2=\"59\" y2=\"19\" stroke=\"#e05050\" stroke-width=\"1.5\"/><line x1=\"5\" y1=\"45\" x2=\"59\" y2=\"45\" stroke=\"#5070e0\" stroke-width=\"1.5\"/><rect x=\"4\" y=\"30\" width=\"56\" height=\"4\" fill=\"#e6e2da\"/><rect x=\"7\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/></svg>",
"tags": [
"breadboard",
"protoboard",
"prototype",
"passive",
"wokwi-breadboard"
],
"properties": [],
"defaultValues": {},
"pinCount": 830,
"$comment": "Velxio-native passive: full 830-point breadboard. id matches the wokwi part type (wokwi-breadboard strips to 'breadboard') so wokwi zip import/export works with no alias. Internal 5-hole strips + power rails are joined in NetlistBuilder/traceDetailed via utils/breadboardNets.ts.",
"id": "breadboard",
"tagName": "velxio-breadboard",
"name": "Breadboard (full)",
"category": "passive",
"description": "830-point solderless breadboard: 63 columns x two 5-hole banks (a-e / f-j) plus four full-length power rails. Holes in the same column strip or rail are electrically connected."
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"8\" y=\"12\" width=\"48\" height=\"40\" rx=\"3\" fill=\"#ffffff\" stroke=\"#c8c4bc\"/><rect x=\"10\" y=\"29\" width=\"44\" height=\"5\" fill=\"#eceae4\"/><rect x=\"12\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/></svg>",
"tags": [
"breadboard",
"mini",
"protoboard",
"passive",
"wokwi-breadboard-mini"
],
"properties": [],
"defaultValues": {},
"pinCount": 170,
"$comment": "Velxio-native passive: 170-point mini breadboard (17x2 banks, no rails). Same wokwi-compatible naming as the full breadboard.",
"id": "breadboard-mini",
"tagName": "velxio-breadboard-mini",
"name": "Breadboard Mini",
"category": "passive",
"description": "170-point mini solderless breadboard: 17 columns x two 5-hole banks (a-e / f-j), no power rails. Holes in the same column strip are electrically connected."
},
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"6\" y=\"29\" width=\"52\" height=\"3\" fill=\"#aaa\"/>\n <ellipse cx=\"32\" cy=\"30\" rx=\"14\" ry=\"9\" fill=\"#165696\"/>\n <ellipse cx=\"28\" cy=\"26\" rx=\"5\" ry=\"3\" fill=\"#3a7cc8\" opacity=\"0.5\"/>\n <text x=\"32\" y=\"56\" text-anchor=\"middle\" font-size=\"10\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">1 nF</text></svg>",
"tags": [
@ -4451,34 +4518,6 @@
"motion",
"sensor"
]
},
{
"id": "ssd1306-i2c-4pin",
"tagName": "velxio-ssd1306-i2c-4pin",
"name": "SSD1306 OLED (I2C, 4-pin)",
"category": "displays",
"pinCount": 4,
"tags": [
"ssd1306",
"oled",
"display",
"i2c",
"4-pin",
"0x3c"
],
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#025caf\" rx=\"6\"/><rect x=\"9\" y=\"24\" width=\"46\" height=\"24\" fill=\"#141414\"/><text x=\"32\" y=\"14\" text-anchor=\"middle\" font-size=\"5\" font-family=\"monospace\" fill=\"#fff\">GND VCC SCL SDA</text><circle cx=\"18\" cy=\"19\" r=\"1.6\" fill=\"#b4aeab\"/><circle cx=\"27\" cy=\"19\" r=\"1.6\" fill=\"#c08540\"/><circle cx=\"37\" cy=\"19\" r=\"1.6\" fill=\"#009e9b\"/><circle cx=\"46\" cy=\"19\" r=\"1.6\" fill=\"#007adb\"/><text x=\"32\" y=\"39\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"46\" text-anchor=\"middle\" font-size=\"4.5\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 4-pin</text></svg>",
"properties": [
{
"name": "i2cAddress",
"type": "string",
"defaultValue": "0x3c",
"control": "text",
"description": "I2C address"
}
],
"defaultValues": {
"i2cAddress": "0x3c"
}
}
]
}

View File

@ -0,0 +1,124 @@
/**
* Breadboard internal-connectivity tests.
*
* A breadboard joins holes in fixed groups (5-hole column strips, power
* rails). The joining is modelled at the union-find level in NetlistBuilder
* (see unionBreadboardGroups) and keyed by utils/breadboardNets.ts these
* tests pin the group semantics and the resulting nets end-to-end through
* buildNetlist / buildWireNetMap.
*/
import { describe, it, expect } from 'vitest';
import { breadboardGroupKey, isBreadboard } from '../utils/breadboardNets';
import { buildNetlist, buildWireNetMap } from '../simulation/spice/NetlistBuilder';
describe('breadboardGroupKey', () => {
it('groups the 5 holes of a column strip', () => {
const keys = ['a', 'b', 'c', 'd', 'e'].map((r) => breadboardGroupKey('breadboard', `18t.${r}`));
expect(new Set(keys).size).toBe(1);
expect(keys[0]).toBe('col18t');
});
it('separates top and bottom banks of the same column', () => {
expect(breadboardGroupKey('breadboard', '18t.a')).not.toBe(
breadboardGroupKey('breadboard', '18b.f'),
);
});
it('separates different columns', () => {
expect(breadboardGroupKey('breadboard', '17t.a')).not.toBe(
breadboardGroupKey('breadboard', '18t.a'),
);
});
it('groups a whole power rail as one net', () => {
expect(breadboardGroupKey('breadboard', 'bn.1')).toBe('railbn');
expect(breadboardGroupKey('breadboard', 'bn.50')).toBe('railbn');
expect(breadboardGroupKey('breadboard', 'bp.3')).toBe('railbp');
expect(breadboardGroupKey('breadboard', 'tp.9')).toBe('railtp');
});
it('works for the mini breadboard and rejects non-breadboards', () => {
expect(breadboardGroupKey('breadboard-mini', '5t.c')).toBe('col5t');
expect(breadboardGroupKey('resistor', '1')).toBeNull();
expect(breadboardGroupKey('breadboard', 'not-a-hole')).toBeNull();
expect(isBreadboard('breadboard')).toBe(true);
expect(isBreadboard('breadboard-mini')).toBe(true);
expect(isBreadboard('pushbutton')).toBe(false);
});
});
describe('breadboard nets in NetlistBuilder', () => {
const BOARD = {
id: 'arduino',
vcc: 5,
pins: { '5V': { type: 'digital', v: 5 }, GND: { type: 'digital', v: 0 } },
groundPinNames: ['GND'],
vccPinNames: ['5V'],
};
it('buildWireNetMap: two wires into the same column strip share a net', () => {
const map = buildWireNetMap({
components: [
{ id: 'bb1', metadataId: 'breadboard', properties: {} },
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
],
wires: [
{
id: 'w-in',
start: { componentId: 'arduino', pinName: '5V' },
end: { componentId: 'bb1', pinName: '10t.a' },
},
{
id: 'w-out',
start: { componentId: 'bb1', pinName: '10t.e' },
end: { componentId: 'r1', pinName: '1' },
},
],
boards: [BOARD],
} as never);
expect(map.get('w-in')).toBeDefined();
expect(map.get('w-in')).toBe(map.get('w-out'));
});
it('buildWireNetMap: different columns stay separate nets', () => {
const map = buildWireNetMap({
components: [{ id: 'bb1', metadataId: 'breadboard', properties: {} }],
wires: [
{
id: 'w-a',
start: { componentId: 'arduino', pinName: '5V' },
end: { componentId: 'bb1', pinName: '10t.a' },
},
{
id: 'w-b',
start: { componentId: 'bb1', pinName: '11t.a' },
end: { componentId: 'arduino', pinName: 'GND' },
},
],
boards: [BOARD],
} as never);
expect(map.get('w-a')).not.toBe(map.get('w-b'));
});
it('buildNetlist: a resistor fed through breadboard strips + rail resolves to vcc/gnd nets', () => {
// 5V → rail tp.1 | rail tp.30 → column 5t.a | 5t.c → R1.1 | R1.2 → 20b.f | 20b.j → GND
const { netlist } = buildNetlist({
components: [
{ id: 'bb1', metadataId: 'breadboard', properties: {} },
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
],
wires: [
{ id: 'w1', start: { componentId: 'arduino', pinName: '5V' }, end: { componentId: 'bb1', pinName: 'tp.1' } },
{ id: 'w2', start: { componentId: 'bb1', pinName: 'tp.30' }, end: { componentId: 'bb1', pinName: '5t.a' } },
{ id: 'w3', start: { componentId: 'bb1', pinName: '5t.c' }, end: { componentId: 'r1', pinName: '1' } },
{ id: 'w4', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'bb1', pinName: '20b.f' } },
{ id: 'w5', start: { componentId: 'bb1', pinName: '20b.j' }, end: { componentId: 'arduino', pinName: 'GND' } },
],
boards: [BOARD],
analysis: { kind: 'op' },
} as never);
// The resistor's card must connect vcc_rail directly to ground — every
// breadboard hop collapsed into the two canonical nets.
expect(netlist).toMatch(/R_r1 (vcc_rail 0|0 vcc_rail) 1000/);
});
});

View File

@ -31,6 +31,7 @@ import { syntheticChipPin } from '../simulation/customChips/syntheticPins';
import { resolveChipNetKey } from '../simulation/customChips/chipNets';
import { getMixedModeScheduler } from '../simulation/spice/MixedModeScheduler';
import { getBoardLogicFamily } from '../simulation/LogicFamilies';
import { breadboardGroupKey } from '../utils/breadboardNets';
// Side-effect imports: register every web component we'll create at runtime.
// `@wokwi/elements` covers the upstream catalog; `../velxio-elements` adds
@ -170,6 +171,29 @@ function traceDetailed(
);
if (result.arduinoPin !== null) return result;
}
// Breadboards join N holes per internal group (5-hole strip / power
// rail), which the 2-terminal PASSIVE_PIN_PAIRS map can't express.
// Continue the trace from every OTHER wired hole in the same group.
const bbGroup = comp && breadboardGroupKey(comp.metadataId, otherEp.pinName);
if (bbGroup && comp) {
const groupPins = new Set<string>();
for (const gw of state.wires) {
for (const ep of [gw.start, gw.end]) {
if (
ep.componentId === comp.id &&
ep.pinName !== otherEp.pinName &&
breadboardGroupKey(comp.metadataId, ep.pinName) === bbGroup
) {
groupPins.add(ep.pinName);
}
}
}
for (const groupPin of groupPins) {
const result = traceDetailed(state, comp.id, groupPin, depth + 1, activeSeen);
if (result.arduinoPin !== null) return result;
}
}
}
}

View File

@ -16,8 +16,40 @@
*/
import { UnionFind } from './unionFind';
import { componentToSpice } from './componentToSpice';
import { isBreadboard, breadboardGroupKey } from '../../utils/breadboardNets';
import type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice } from './types';
/**
* Union the wired pins of every breadboard that share an internal group
* (5-hole column or power rail). Breadboard connectivity is static, so it is
* modelled at the union-find level no SPICE cards needed which makes it
* equally visible to buildNetlist, the circuit verifier, and the overlay
* net maps (all of which run their own bare union-find over wires).
*/
function unionBreadboardGroups(
uf: UnionFind,
components: ReadonlyArray<{ id: string; metadataId: string }>,
wires: WireForSpice[],
pinKey: (componentId: string, pinName: string) => string,
): void {
for (const comp of components) {
if (!isBreadboard(comp.metadataId)) continue;
const anchors = new Map<string, string>(); // groupKey → first pin key
for (const pinName of pinsReferencedByWires(comp.id, wires)) {
const group = breadboardGroupKey(comp.metadataId, pinName);
if (!group) continue;
const key = pinKey(comp.id, pinName);
uf.add(key);
const anchor = anchors.get(group);
if (anchor) {
uf.union(anchor, key);
} else {
anchors.set(group, key);
}
}
}
}
// Matches GND, VSS, VEE, GROUND and any numbered ground pin in the common
// spellings boards actually emit: GND.1 / GND.2 (dotted), GND2 / GND3 (bare),
// GND_2 (underscore). Several dev-kit board elements label their extra ground
@ -104,6 +136,9 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult {
}
}
// Breadboards: join wired holes that share an internal strip/rail.
unionBreadboardGroups(uf, components, wires, pinKey);
// ── 2. Canonicalize ground / VCC pins ────────────────────────────────────
for (const board of boards) {
for (const pinName of board.groundPinNames ?? []) {
@ -457,6 +492,9 @@ export function buildWireNetMap(
uf.union(a, b);
}
// Breadboards: join wired holes that share an internal strip/rail.
unionBreadboardGroups(uf, components, wires, pin);
for (const board of boards) {
for (const pName of board.groundPinNames ?? []) uf.setCanonical(pin(board.id, pName), '0');
for (const pName of board.vccPinNames ?? []) uf.setCanonical(pin(board.id, pName), 'vcc_rail');
@ -504,6 +542,9 @@ export function buildBoardPinNetMap(
uf.union(a, b);
}
// Breadboards: join wired holes that share an internal strip/rail.
unionBreadboardGroups(uf, components, wires, pin);
// Canonicalize board ground/vcc pins (from boardPinGroups metadata)
for (const board of boards) {
for (const pName of board.groundPinNames ?? []) {

View File

@ -0,0 +1,41 @@
/**
* Breadboard internal-connectivity helpers.
*
* A solderless breadboard electrically joins holes in fixed groups:
* - each terminal-strip column of 5 holes is one net
* (full board: rows a-e form the top bank, f-j the bottom bank), and
* - each power rail (full board only) is one net running the whole length.
*
* Pin names follow the Wokwi convention so imported diagram.json wires work
* verbatim (e.g. "18t.d" = column 18, top bank, row d; "bn.15" = bottom
* negative rail, hole 15):
* holes: `${col}t.${a-e}` | `${col}b.${f-j}`
* rails: `tp.N` / `tn.N` (top +/-) | `bp.N` / `bn.N` (bottom +/-)
*
* Consumers (NetlistBuilder union-find, DynamicComponent digital trace) use
* `breadboardGroupKey` to decide which pins are internally shorted: two pins
* on the same breadboard belong to the same net iff their group keys match.
*/
const BREADBOARD_IDS = new Set(['breadboard', 'breadboard-mini']);
/** True when the metadataId is one of the breadboard parts. */
export function isBreadboard(metadataId: string): boolean {
return BREADBOARD_IDS.has(metadataId);
}
const HOLE_RE = /^(\d+)([tb])\.([a-j])$/;
const RAIL_RE = /^([tb][pn])\.(\d+)$/;
/**
* Group key for a breadboard pin, or null when the pin name is not a valid
* breadboard hole/rail. Pins with equal keys are internally connected.
*/
export function breadboardGroupKey(metadataId: string, pinName: string): string | null {
if (!isBreadboard(metadataId)) return null;
const hole = HOLE_RE.exec(pinName);
if (hole) return `col${hole[1]}${hole[2]}`; // e.g. "col18t" — one 5-hole strip
const rail = RAIL_RE.exec(pinName);
if (rail) return `rail${rail[1]}`; // e.g. "railbn" — the whole rail is one net
return null;
}

View File

@ -0,0 +1,145 @@
/**
* <velxio-breadboard> full-size 830-point solderless breadboard.
*
* 63 terminal-strip columns × two 5-hole banks (rows a-e top, f-j bottom)
* plus four power rails (+/- top and bottom, 50 holes each in 5-hole groups).
* Pin names follow the Wokwi convention (see utils/breadboardNets.ts):
* holes `${col}t.${a-e}` / `${col}b.${f-j}`, rails `tp.N` `tn.N` `bp.N` `bn.N`.
*
* Purely passive: the electrical joining of each column/rail happens in
* NetlistBuilder + the digital trace via breadboardGroupKey this element
* only renders and exposes pinInfo. The pinInfo array is precomputed at
* module load (830 entries) so the getter stays cheap for the pin overlay,
* which re-reads it on every measure (see CLAUDE.md §6a).
*/
const PITCH = 9.6; // 0.1in in wokwi-elements CSS px
const COLS = 63;
const RAIL_HOLES = 50; // 10 groups of 5
// Layout (CSS px). Terminal grid is centered; rails hug the long edges.
const GRID_LEFT = 19.2; // x of column 1
const RAIL_LEFT = GRID_LEFT + ((COLS - 1) * PITCH - (RAIL_HOLES - 1 + 9) * PITCH) / 2;
const ROW_Y: Record<string, number> = {
// top rails
'tp': 16.8,
'tn': 26.4,
// bank a-e
a: 55.2, b: 64.8, c: 74.4, d: 84.0, e: 93.6,
// bank f-j (below the center channel)
f: 122.4, g: 132.0, h: 141.6, i: 151.2, j: 160.8,
// bottom rails
'bp': 189.6,
'bn': 199.2,
};
const WIDTH = GRID_LEFT * 2 + (COLS - 1) * PITCH; // 614.4
const HEIGHT = 216;
function railX(n: number): number {
// 10 groups of 5 with one extra pitch between groups
const idx = n - 1;
return RAIL_LEFT + (idx + Math.floor(idx / 5)) * PITCH;
}
export interface BreadboardPin {
name: string;
x: number;
y: number;
number: number;
signals: [];
}
function buildPins(): BreadboardPin[] {
const pins: BreadboardPin[] = [];
let n = 1;
for (const rail of ['tp', 'tn', 'bp', 'bn'] as const) {
for (let i = 1; i <= RAIL_HOLES; i++) {
pins.push({ name: `${rail}.${i}`, x: railX(i), y: ROW_Y[rail], number: n++, signals: [] });
}
}
for (let col = 1; col <= COLS; col++) {
const x = GRID_LEFT + (col - 1) * PITCH;
for (const row of ['a', 'b', 'c', 'd', 'e'] as const) {
pins.push({ name: `${col}t.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
}
for (const row of ['f', 'g', 'h', 'i', 'j'] as const) {
pins.push({ name: `${col}b.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
}
}
return pins;
}
const PINS: readonly BreadboardPin[] = buildPins();
function holeRect(x: number, y: number): string {
return `<rect x="${(x - 1.6).toFixed(1)}" y="${(y - 1.6).toFixed(1)}" width="3.2" height="3.2" rx="0.8" fill="#333"/>`;
}
function buildSvg(): string {
const parts: string[] = [];
parts.push(
`<svg width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}" xmlns="http://www.w3.org/2000/svg">`,
// body
`<rect x="0" y="0" width="${WIDTH}" height="${HEIGHT}" rx="6" fill="#f4f1ec" stroke="#d4d0c8"/>`,
// rail separator lines: red above +, blue below -
`<line x1="8" y1="9.6" x2="${WIDTH - 8}" y2="9.6" stroke="#e05050" stroke-width="1.6"/>`,
`<line x1="8" y1="33.6" x2="${WIDTH - 8}" y2="33.6" stroke="#5070e0" stroke-width="1.6"/>`,
`<line x1="8" y1="182.4" x2="${WIDTH - 8}" y2="182.4" stroke="#e05050" stroke-width="1.6"/>`,
`<line x1="8" y1="206.4" x2="${WIDTH - 8}" y2="206.4" stroke="#5070e0" stroke-width="1.6"/>`,
// center channel
`<rect x="4" y="${101.5}" width="${WIDTH - 8}" height="13" rx="2" fill="#e6e2da"/>`,
);
// holes
for (const p of PINS) parts.push(holeRect(p.x, p.y));
// column numbers every 5 columns, in both banks' margins
for (let col = 5; col <= COLS; col += 5) {
const x = GRID_LEFT + (col - 1) * PITCH;
parts.push(
`<text x="${x}" y="${47.5}" font-size="6.5" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${col}</text>`,
`<text x="${x}" y="${174}" font-size="6.5" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${col}</text>`,
);
}
// row letters on both sides
for (const row of ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']) {
for (const x of [8.5, WIDTH - 8.5]) {
parts.push(
`<text x="${x}" y="${ROW_Y[row] + 2.3}" font-size="6.5" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${row}</text>`,
);
}
}
// rail +/- labels
for (const [rail, sym, color] of [
['tp', '+', '#e05050'], ['tn', '', '#5070e0'],
['bp', '+', '#e05050'], ['bn', '', '#5070e0'],
] as const) {
for (const x of [8.5, WIDTH - 8.5]) {
parts.push(
`<text x="${x}" y="${ROW_Y[rail] + 2.6}" font-size="8" fill="${color}" text-anchor="middle" font-family="sans-serif">${sym}</text>`,
);
}
}
parts.push('</svg>');
return parts.join('');
}
let svgCache: string | null = null;
export class BreadboardElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
if (!svgCache) svgCache = buildSvg();
this.shadowRoot!.innerHTML = svgCache;
}
get pinInfo() {
return PINS as unknown as Array<{ name: string; x: number; y: number; number: number }>;
}
}
if (!customElements.get('velxio-breadboard')) {
customElements.define('velxio-breadboard', BreadboardElement);
}

View File

@ -0,0 +1,98 @@
/**
* <velxio-breadboard-mini> 170-point mini solderless breadboard.
*
* 17 terminal-strip columns × two 5-hole banks (rows a-e top, f-j bottom),
* no power rails. Pin names follow the Wokwi convention:
* `${col}t.${a-e}` / `${col}b.${f-j}` (see utils/breadboardNets.ts).
*
* Passive internal column joining happens in NetlistBuilder / the digital
* trace via breadboardGroupKey. pinInfo (170 entries) is precomputed at
* module load so the getter stays cheap for the pin overlay.
*/
const PITCH = 9.6;
const COLS = 17;
const GRID_LEFT = 14.4;
const ROW_Y: Record<string, number> = {
a: 14.4, b: 24.0, c: 33.6, d: 43.2, e: 52.8,
f: 76.8, g: 86.4, h: 96.0, i: 105.6, j: 115.2,
};
const WIDTH = GRID_LEFT * 2 + (COLS - 1) * PITCH; // 182.4
const HEIGHT = 129.6;
export interface BreadboardMiniPin {
name: string;
x: number;
y: number;
number: number;
signals: [];
}
function buildPins(): BreadboardMiniPin[] {
const pins: BreadboardMiniPin[] = [];
let n = 1;
for (let col = 1; col <= COLS; col++) {
const x = GRID_LEFT + (col - 1) * PITCH;
for (const row of ['a', 'b', 'c', 'd', 'e'] as const) {
pins.push({ name: `${col}t.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
}
for (const row of ['f', 'g', 'h', 'i', 'j'] as const) {
pins.push({ name: `${col}b.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
}
}
return pins;
}
const PINS: readonly BreadboardMiniPin[] = buildPins();
function buildSvg(): string {
const parts: string[] = [];
parts.push(
`<svg width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}" xmlns="http://www.w3.org/2000/svg">`,
`<rect x="0" y="0" width="${WIDTH}" height="${HEIGHT}" rx="5" fill="#ffffff" stroke="#d4d0c8"/>`,
`<rect x="3" y="${59.5}" width="${WIDTH - 6}" height="11" rx="2" fill="#eceae4"/>`,
);
for (const p of PINS) {
parts.push(
`<rect x="${(p.x - 1.6).toFixed(1)}" y="${(p.y - 1.6).toFixed(1)}" width="3.2" height="3.2" rx="0.8" fill="#333"/>`,
);
}
for (let col = 5; col <= COLS; col += 5) {
const x = GRID_LEFT + (col - 1) * PITCH;
parts.push(
`<text x="${x}" y="${7.8}" font-size="6" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${col}</text>`,
`<text x="${x}" y="${125.5}" font-size="6" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${col}</text>`,
);
}
for (const row of Object.keys(ROW_Y)) {
for (const x of [5.5, WIDTH - 5.5]) {
parts.push(
`<text x="${x}" y="${ROW_Y[row] + 2.2}" font-size="6" fill="#8a8578" text-anchor="middle" font-family="sans-serif">${row}</text>`,
);
}
}
parts.push('</svg>');
return parts.join('');
}
let svgCache: string | null = null;
export class BreadboardMiniElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
if (!svgCache) svgCache = buildSvg();
this.shadowRoot!.innerHTML = svgCache;
}
get pinInfo() {
return PINS as unknown as Array<{ name: string; x: number; y: number; number: number }>;
}
}
if (!customElements.get('velxio-breadboard-mini')) {
customElements.define('velxio-breadboard-mini', BreadboardMiniElement);
}

View File

@ -18,7 +18,11 @@ import './capacitor-element';
import './capacitor-electrolytic-element';
import './inductor-element';
import './custom-chip-element';
import './breadboard-element';
import './breadboard-mini-element';
export { CapacitorElement } from './capacitor-element';
export { CapacitorElectrolyticElement } from './capacitor-electrolytic-element';
export { InductorElement } from './inductor-element';
export { BreadboardElement } from './breadboard-element';
export { BreadboardMiniElement } from './breadboard-mini-element';

View File

@ -2484,6 +2484,44 @@
"defaultValues": {
"i2cAddress": "0x3c"
}
},
{
"$comment": "Velxio-native passive: full 830-point breadboard. id matches the wokwi part type (wokwi-breadboard strips to 'breadboard') so wokwi zip import/export works with no alias. Internal 5-hole strips + power rails are joined in NetlistBuilder/traceDetailed via utils/breadboardNets.ts.",
"id": "breadboard",
"tagName": "velxio-breadboard",
"name": "Breadboard (full)",
"category": "passive",
"pinCount": 830,
"tags": [
"breadboard",
"protoboard",
"prototype",
"passive",
"wokwi-breadboard"
],
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"2\" y=\"14\" width=\"60\" height=\"36\" rx=\"3\" fill=\"#f4f1ec\" stroke=\"#c8c4bc\"/><line x1=\"5\" y1=\"19\" x2=\"59\" y2=\"19\" stroke=\"#e05050\" stroke-width=\"1.5\"/><line x1=\"5\" y1=\"45\" x2=\"59\" y2=\"45\" stroke=\"#5070e0\" stroke-width=\"1.5\"/><rect x=\"4\" y=\"30\" width=\"56\" height=\"4\" fill=\"#e6e2da\"/><rect x=\"7\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"7\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"12\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"17\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"22\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"27\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"32\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"37\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"42\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"47\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"23\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"26\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"36\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/><rect x=\"52\" y=\"39\" width=\"2.4\" height=\"2.4\" fill=\"#333\"/></svg>",
"properties": [],
"defaultValues": {},
"description": "830-point solderless breadboard: 63 columns x two 5-hole banks (a-e / f-j) plus four full-length power rails. Holes in the same column strip or rail are electrically connected."
},
{
"$comment": "Velxio-native passive: 170-point mini breadboard (17x2 banks, no rails). Same wokwi-compatible naming as the full breadboard.",
"id": "breadboard-mini",
"tagName": "velxio-breadboard-mini",
"name": "Breadboard Mini",
"category": "passive",
"pinCount": 170,
"tags": [
"breadboard",
"mini",
"protoboard",
"passive",
"wokwi-breadboard-mini"
],
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"8\" y=\"12\" width=\"48\" height=\"40\" rx=\"3\" fill=\"#ffffff\" stroke=\"#c8c4bc\"/><rect x=\"10\" y=\"29\" width=\"44\" height=\"5\" fill=\"#eceae4\"/><rect x=\"12\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"12\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"17\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"22\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"27\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"32\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"37\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"42\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"47\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"17\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"22\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"38\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/><rect x=\"52\" y=\"43\" width=\"2.6\" height=\"2.6\" fill=\"#333\"/></svg>",
"properties": [],
"defaultValues": {},
"description": "170-point mini solderless breadboard: 17 columns x two 5-hole banks (a-e / f-j), no power rails. Holes in the same column strip are electrically connected."
}
],
"led": {