diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 0f0ad0fe..c0ff32d9 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1450,7 +1450,7 @@ "defaultValues": { "i2cAddress": "0x3c" }, - "pinCount": 8, + "pinCount": 0, "tags": [ "ssd1306", "oled", @@ -1460,6 +1460,35 @@ "0x3c" ] }, + { + "thumbnail": "GND VCC SCL SDASSD1306I2C 4-pin", + "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": "\n \n \n MOTOR-DRIVER-L293D\n \n ", "tags": [ @@ -3325,6 +3354,44 @@ "led" ] }, + { + "thumbnail": "", + "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": "", + "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": "\n \n \n \n 1 nF", "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": "GND VCC SCL SDASSD1306I2C 4-pin", - "properties": [ - { - "name": "i2cAddress", - "type": "string", - "defaultValue": "0x3c", - "control": "text", - "description": "I2C address" - } - ], - "defaultValues": { - "i2cAddress": "0x3c" - } } ] } \ No newline at end of file diff --git a/frontend/src/__tests__/breadboard-nets.test.ts b/frontend/src/__tests__/breadboard-nets.test.ts new file mode 100644 index 00000000..e8e34377 --- /dev/null +++ b/frontend/src/__tests__/breadboard-nets.test.ts @@ -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/); + }); +}); diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index b6b114cd..ec2f55f6 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -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(); + 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; + } + } } } diff --git a/frontend/src/simulation/spice/NetlistBuilder.ts b/frontend/src/simulation/spice/NetlistBuilder.ts index a966fc28..968367a6 100644 --- a/frontend/src/simulation/spice/NetlistBuilder.ts +++ b/frontend/src/simulation/spice/NetlistBuilder.ts @@ -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(); // 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 ?? []) { diff --git a/frontend/src/utils/breadboardNets.ts b/frontend/src/utils/breadboardNets.ts new file mode 100644 index 00000000..7f2d264a --- /dev/null +++ b/frontend/src/utils/breadboardNets.ts @@ -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; +} diff --git a/frontend/src/velxio-elements/breadboard-element.ts b/frontend/src/velxio-elements/breadboard-element.ts new file mode 100644 index 00000000..881adb02 --- /dev/null +++ b/frontend/src/velxio-elements/breadboard-element.ts @@ -0,0 +1,145 @@ +/** + * — 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 = { + // 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 ``; +} + +function buildSvg(): string { + const parts: string[] = []; + parts.push( + ``, + // body + ``, + // rail separator lines: red above +, blue below - + ``, + ``, + ``, + ``, + // center channel + ``, + ); + // 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( + `${col}`, + `${col}`, + ); + } + // 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( + `${row}`, + ); + } + } + // 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( + `${sym}`, + ); + } + } + parts.push(''); + 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); +} diff --git a/frontend/src/velxio-elements/breadboard-mini-element.ts b/frontend/src/velxio-elements/breadboard-mini-element.ts new file mode 100644 index 00000000..28fa8a72 --- /dev/null +++ b/frontend/src/velxio-elements/breadboard-mini-element.ts @@ -0,0 +1,98 @@ +/** + * — 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 = { + 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( + ``, + ``, + ``, + ); + for (const p of PINS) { + parts.push( + ``, + ); + } + for (let col = 5; col <= COLS; col += 5) { + const x = GRID_LEFT + (col - 1) * PITCH; + parts.push( + `${col}`, + `${col}`, + ); + } + for (const row of Object.keys(ROW_Y)) { + for (const x of [5.5, WIDTH - 5.5]) { + parts.push( + `${row}`, + ); + } + } + parts.push(''); + 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); +} diff --git a/frontend/src/velxio-elements/index.ts b/frontend/src/velxio-elements/index.ts index 495921e5..714409fd 100644 --- a/frontend/src/velxio-elements/index.ts +++ b/frontend/src/velxio-elements/index.ts @@ -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'; diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json index b69ea800..3c0bc741 100644 --- a/scripts/component-overrides.json +++ b/scripts/component-overrides.json @@ -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": "", + "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": "", + "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": {