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:
parent
814755b249
commit
6307aed007
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ?? []) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue