diff --git a/frontend/src/assets/Raspberry_Pi_1_illustration.svg b/frontend/src/assets/Raspberry_Pi_1_illustration.svg
new file mode 100644
index 00000000..b87e05bd
--- /dev/null
+++ b/frontend/src/assets/Raspberry_Pi_1_illustration.svg
@@ -0,0 +1,180 @@
+
diff --git a/frontend/src/assets/Raspberry_Pi_2_illustration.svg b/frontend/src/assets/Raspberry_Pi_2_illustration.svg
new file mode 100644
index 00000000..7b93a59b
--- /dev/null
+++ b/frontend/src/assets/Raspberry_Pi_2_illustration.svg
@@ -0,0 +1,180 @@
+
diff --git a/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg b/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg
new file mode 100644
index 00000000..44852ad8
--- /dev/null
+++ b/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg
@@ -0,0 +1,149 @@
+
diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx
index 45f8c233..7a6262f5 100644
--- a/frontend/src/components/ComponentPickerModal.tsx
+++ b/frontend/src/components/ComponentPickerModal.tsx
@@ -47,6 +47,9 @@ import {
type OnlineOnlyBoardAd,
type OnlineOnlyComponentAd,
} from '../lib/onlineOnlyBoards';
+import raspberryPiZeroSvg from '../assets/Raspberry_Pi_Zero_illustration.svg';
+import raspberryPi1Svg from '../assets/Raspberry_Pi_1_illustration.svg';
+import raspberryPi2Svg from '../assets/Raspberry_Pi_2_illustration.svg';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import raspberryPi4Png from '../assets/raspberry-pi-4-board.png';
import raspberryPi5Png from '../assets/raspberry-pi-5-board.png';
@@ -821,15 +824,18 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => {
};
}, [kind]);
- const reactThumbnail =
- // Zero/1/2 render on canvas through the Pi-3 element (same 40-pin art),
- // so their cards reuse the same illustration.
- kind === 'raspberry-pi-3' ||
- kind === 'raspberry-pi-zero' ||
- kind === 'raspberry-pi-1' ||
- kind === 'raspberry-pi-2' ? (
+ // Every Pi shows its own board. The cards used to share the Pi 3's picture,
+ // which made the picker claim a Zero looks like a Pi 3.
+ const piArt: Partial> = {
+ 'raspberry-pi-zero': raspberryPiZeroSvg,
+ 'raspberry-pi-1': raspberryPi1Svg,
+ 'raspberry-pi-2': raspberryPi2Svg,
+ 'raspberry-pi-3': raspberryPi3Svg,
+ };
+
+ const reactThumbnail = piArt[kind] ? (
diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx
index a7db42eb..bd7c61fe 100644
--- a/frontend/src/components/simulator/BoardOnCanvas.tsx
+++ b/frontend/src/components/simulator/BoardOnCanvas.tsx
@@ -10,6 +10,9 @@ import { ArduinoMega } from '../velxio-components/ArduinoMega';
// boardKind by mistake — kept the import out so future contributors don't
// re-wire it back in. If someone genuinely needs a Nano RP2040 Connect
// board (D2..D13 labels), add a new boardKind 'arduino-nano-rp2040'.
+import { RaspberryPiZero } from '../velxio-components/RaspberryPiZero';
+import { RaspberryPi1 } from '../velxio-components/RaspberryPi1';
+import { RaspberryPi2 } from '../velxio-components/RaspberryPi2';
import { RaspberryPi3 } from '../velxio-components/RaspberryPi3';
import { RaspberryPi4 } from '../velxio-components/RaspberryPi4';
import { RaspberryPi5 } from '../velxio-components/RaspberryPi5';
@@ -167,12 +170,18 @@ export const BoardOnCanvas = ({
case 'raspberry-pi-pico':
case 'pi-pico-w':
return ;
- // Zero/1/2 share the Pi-3's 40-pin board art (their canvas identity is
- // the header, and the ComponentRegistry metadata reuses the same tag);
- // only the backend QEMU profile differs per kind.
+ // Each Pi draws as itself. Zero/1/2 used to borrow the Pi 3's picture
+ // "because the header is the canvas identity" — but the Zero is 65x30 mm
+ // against the Pi 3's 85x56, ships its header unpopulated and has neither
+ // Ethernet nor full-size USB, so that shortcut drew a board the student
+ // was not holding. 1B+ and 2B do share the Pi 3's outline; their silicon
+ // and silkscreen do not.
case 'raspberry-pi-zero':
+ return ;
case 'raspberry-pi-1':
+ return ;
case 'raspberry-pi-2':
+ return ;
case 'raspberry-pi-3':
return ;
case 'raspberry-pi-4':
diff --git a/frontend/src/components/velxio-components/RaspberryPi1.tsx b/frontend/src/components/velxio-components/RaspberryPi1.tsx
new file mode 100644
index 00000000..b0ca43f5
--- /dev/null
+++ b/frontend/src/components/velxio-components/RaspberryPi1.tsx
@@ -0,0 +1,66 @@
+import { useEffect, useRef } from 'react';
+import boardSvg from '../../assets/Raspberry_Pi_1_illustration.svg';
+import { buildPi40PinHeader } from './pi40PinHeader';
+
+/**
+ * Raspberry Pi 1 Model B+ — Velxio board art.
+ *
+ * Shares the 85 x 56 mm "B+ form factor" with the Pi 3, which is why this
+ * board used to be drawn AS a Pi 3. The outline and the connector positions
+ * really are identical; the silicon is not — BCM2835 with the RAM stacked on top and no separate Elpida package — and neither is the
+ * silkscreen, so the picture now says which board the student is holding.
+ *
+ * Geometry is in the SVG's own units (10 per mm), the same numbers the art is
+ * drawn from, so the pin tips land on the drawn pads. Pitch is a true 2.54 mm.
+ */
+
+const SVG_VB_W = 884; // 85 mm of PCB + the USB/Ethernet overhang on the right
+const SVG_VB_H = 578; // 56 mm of PCB + the connector overhang at the bottom
+const HDR_X0 = 72;
+const HDR_STEP = 25.4;
+const HDR_Y_TOP = 42;
+const HDR_Y_BOT = 67.4;
+
+const DISPLAY_W = 330;
+const SCALE = DISPLAY_W / SVG_VB_W;
+const DISPLAY_H = Math.round(SVG_VB_H * SCALE);
+
+interface Props {
+ id?: string;
+ x?: number;
+ y?: number;
+}
+
+export const RaspberryPi1 = ({ id = 'raspberry-pi-1', x = 0, y = 0 }: Props) => {
+ const imgRef = useRef(null);
+
+ useEffect(() => {
+ if (imgRef.current) {
+ (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({
+ xStart: HDR_X0 * SCALE,
+ xStep: HDR_STEP * SCALE,
+ yTop: HDR_Y_TOP * SCALE,
+ yBot: HDR_Y_BOT * SCALE,
+ });
+ }
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/velxio-components/RaspberryPi2.tsx b/frontend/src/components/velxio-components/RaspberryPi2.tsx
new file mode 100644
index 00000000..7f018a00
--- /dev/null
+++ b/frontend/src/components/velxio-components/RaspberryPi2.tsx
@@ -0,0 +1,66 @@
+import { useEffect, useRef } from 'react';
+import boardSvg from '../../assets/Raspberry_Pi_2_illustration.svg';
+import { buildPi40PinHeader } from './pi40PinHeader';
+
+/**
+ * Raspberry Pi 2 Model B — Velxio board art.
+ *
+ * Shares the 85 x 56 mm "B+ form factor" with the Pi 3, which is why this
+ * board used to be drawn AS a Pi 3. The outline and the connector positions
+ * really are identical; the silicon is not — BCM2836, the first quad-core, with its RAM moved to the underside — and neither is the
+ * silkscreen, so the picture now says which board the student is holding.
+ *
+ * Geometry is in the SVG's own units (10 per mm), the same numbers the art is
+ * drawn from, so the pin tips land on the drawn pads. Pitch is a true 2.54 mm.
+ */
+
+const SVG_VB_W = 884; // 85 mm of PCB + the USB/Ethernet overhang on the right
+const SVG_VB_H = 578; // 56 mm of PCB + the connector overhang at the bottom
+const HDR_X0 = 72;
+const HDR_STEP = 25.4;
+const HDR_Y_TOP = 42;
+const HDR_Y_BOT = 67.4;
+
+const DISPLAY_W = 330;
+const SCALE = DISPLAY_W / SVG_VB_W;
+const DISPLAY_H = Math.round(SVG_VB_H * SCALE);
+
+interface Props {
+ id?: string;
+ x?: number;
+ y?: number;
+}
+
+export const RaspberryPi2 = ({ id = 'raspberry-pi-2', x = 0, y = 0 }: Props) => {
+ const imgRef = useRef(null);
+
+ useEffect(() => {
+ if (imgRef.current) {
+ (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({
+ xStart: HDR_X0 * SCALE,
+ xStep: HDR_STEP * SCALE,
+ yTop: HDR_Y_TOP * SCALE,
+ yBot: HDR_Y_BOT * SCALE,
+ });
+ }
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/velxio-components/RaspberryPiZero.tsx b/frontend/src/components/velxio-components/RaspberryPiZero.tsx
new file mode 100644
index 00000000..c19bfa9a
--- /dev/null
+++ b/frontend/src/components/velxio-components/RaspberryPiZero.tsx
@@ -0,0 +1,74 @@
+import { useEffect, useRef } from 'react';
+import raspberryPiZeroSvg from '../../assets/Raspberry_Pi_Zero_illustration.svg';
+import { buildPi40PinHeader } from './pi40PinHeader';
+
+/**
+ * Raspberry Pi Zero 2 W — Velxio board art.
+ *
+ * The Zero used to borrow the Pi 3's picture. That is wrong in the one way a
+ * board picture can be wrong: it is a different SIZE. The Zero is 65 x 30 mm
+ * against the Pi 3's 85 x 56 mm, its header ships unpopulated, and it has no
+ * Ethernet and no full-size USB. A canvas that draws it as a Pi 3 tells the
+ * student the opposite of what the hardware looks like.
+ *
+ * Geometry below is in the SVG's own units (10 per mm) and is the SAME set of
+ * numbers the artwork is drawn from, so the pin tips cannot drift off the
+ * pads. Header pitch is a true 2.54 mm.
+ */
+
+const SVG_W = 650; // 65 mm
+const SVG_VB_H = 314; // 30 mm of PCB + the connector overhang at the bottom
+const HDR_X0 = 82;
+const HDR_STEP = 25.4;
+const HDR_Y_TOP = 31;
+const HDR_Y_BOT = 56.4;
+
+// 65 mm next to the Pi 3's 85 mm at 320 px: 320 * 65 / 85 ~ 245. Round to 250
+// so the Zero reads as the small board it is when both sit on one canvas.
+const DISPLAY_W = 250;
+const SCALE = DISPLAY_W / SVG_W;
+const DISPLAY_H = Math.round(SVG_VB_H * SCALE);
+
+interface RaspberryPiZeroProps {
+ id?: string;
+ x?: number;
+ y?: number;
+}
+
+export const RaspberryPiZero = ({
+ id = 'raspberry-pi-zero',
+ x = 0,
+ y = 0,
+}: RaspberryPiZeroProps) => {
+ const imgRef = useRef(null);
+
+ useEffect(() => {
+ if (imgRef.current) {
+ (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({
+ xStart: HDR_X0 * SCALE,
+ xStep: HDR_STEP * SCALE,
+ yTop: HDR_Y_TOP * SCALE,
+ yBot: HDR_Y_BOT * SCALE,
+ });
+ }
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/velxio-components/pi40PinHeader.ts b/frontend/src/components/velxio-components/pi40PinHeader.ts
index 8b7954de..8e2e63e4 100644
--- a/frontend/src/components/velxio-components/pi40PinHeader.ts
+++ b/frontend/src/components/velxio-components/pi40PinHeader.ts
@@ -4,9 +4,12 @@
* BCM GPIO assignment, same naming convention — so wires drawn against
* a Pi 3 example transfer to a Pi 4 or Pi 5 board without re-routing.
*
- * Coordinates are in CSS pixels relative to the board element's top-left,
- * sized to fit the 250×160 footprint shared by every Velxio Pi element.
- * Pin "1" sits top-left; odd pins on the top row, even on the bottom.
+ * Coordinates are in CSS pixels relative to the board element's top-left.
+ * Every Pi draws its header at a different place on its own artwork, so the
+ * geometry is a parameter: pass the header origin, the column pitch and the
+ * two row offsets AS MEASURED ON THE ART, and get pin tips that land on the
+ * drawn pads. Pin "1" sits top-left; odd pins on the top row, even on the
+ * bottom.
*
* Names follow the Pi 3 wrapper convention (BCM GPIO numbers like
* "GPIO14" plus power/ground labels) so example wires using those names
@@ -20,10 +23,24 @@ export interface PinInfo {
signals: string[];
}
-const PIN_X_START = 20;
-const PIN_X_STEP = 10;
-const PIN_Y_TOP = 10;
-const PIN_Y_BOT = 20;
+export interface Pi40HeaderGeometry {
+ /** x of pin 1 / pin 2 (column 0), in CSS pixels. */
+ xStart: number;
+ /** Column pitch in CSS pixels — 2.54 mm scaled by the art's own scale. */
+ xStep: number;
+ /** y of the odd (top) row. */
+ yTop: number;
+ /** y of the even (bottom) row. */
+ yBot: number;
+}
+
+/** The 250x160 footprint the first Velxio Pi element was drawn at. */
+const DEFAULT_GEOMETRY: Pi40HeaderGeometry = {
+ xStart: 20,
+ xStep: 10,
+ yTop: 10,
+ yBot: 20,
+};
const PI_PIN_NAMES: Record = {
1: '3V3', 2: '5V',
@@ -48,22 +65,25 @@ const PI_PIN_NAMES: Record = {
39: 'GND', 40: 'GPIO21',
};
-export function buildPi40PinHeader(): PinInfo[] {
+export function buildPi40PinHeader(
+ geometry: Pi40HeaderGeometry = DEFAULT_GEOMETRY,
+): PinInfo[] {
+ const { xStart, xStep, yTop, yBot } = geometry;
const pins: PinInfo[] = [];
for (let col = 0; col < 20; col++) {
const oddPin = col * 2 + 1; // top row
const evenPin = col * 2 + 2; // bottom row
- const px = PIN_X_START + col * PIN_X_STEP;
+ const px = xStart + col * xStep;
pins.push({
name: PI_PIN_NAMES[oddPin] ?? `P${oddPin}`,
x: px,
- y: PIN_Y_TOP,
+ y: yTop,
signals: [],
});
pins.push({
name: PI_PIN_NAMES[evenPin] ?? `P${evenPin}`,
x: px,
- y: PIN_Y_BOT,
+ y: yBot,
signals: [],
});
}