velxio/frontend/src/velxio-elements/breadboard-element.ts

146 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* <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);
}