velxio/frontend/src/components/simulator/BoardOnCanvas.tsx

237 lines
8.6 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.

import React from 'react';
import type { BoardInstance } from '../../types/board';
import { ArduinoUno } from '../velxio-components/ArduinoUno';
import { ArduinoNano } from '../velxio-components/ArduinoNano';
import { ArduinoMega } from '../velxio-components/ArduinoMega';
// NanoRP2040 (wokwi-nano-rp2040-connect) used to back the 'raspberry-pi-pico'
// 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 { RaspberryPi3 } from '../velxio-components/RaspberryPi3';
import { RaspberryPi4 } from '../velxio-components/RaspberryPi4';
import { RaspberryPi5 } from '../velxio-components/RaspberryPi5';
import { Esp32 } from '../velxio-components/Esp32';
import { Attiny85 } from '../velxio-components/Attiny85';
import { PiPicoW } from '../velxio-components/PiPicoW';
import {
Stm32BluePill,
Stm32BlackPill,
Stm32BluePillF103CB,
Stm32BlackPillF401,
Stm32F4Discovery,
Stm32OlimexH405,
Stm32NetduinoPlus2,
Stm32Netduino2,
} from '../velxio-components/Stm32BluePill';
import { PinOverlay } from './PinOverlay';
// Board visual dimensions (width × height) for the drag-overlay sizing.
// ESP32 sizes match the wokwi-boards SVG rendered at 5 px/mm.
const BOARD_SIZE: Record<string, { w: number; h: number }> = {
// wokwi-elements: rendered at 96 dpi — 1mm = 3.7795px
'arduino-uno': { w: 274, h: 202 }, // 72.58mm × 53.34mm
'arduino-nano': { w: 170, h: 67 }, // 44.9mm × 17.8mm
'arduino-mega': { w: 388, h: 192 }, // 102.66mm × 50.80mm
// Pi Pico physical board is 51mm × 21mm vertical-narrow. The render
// uses velxio's <velxio-pi-pico-w>, same Web Component as 'pi-pico-w'
// because the Pico and Pico W are pin-compatible. Used to render the
// wokwi-nano-rp2040-connect (168×68) — that was a completely different
// board with D2-D13 pin labels, so wires in pico examples that
// referenced GP10/GP18/etc. landed at (0,0). The render now matches
// the boardKind name.
'raspberry-pi-pico': { w: 105, h: 264 },
'raspberry-pi-3': { w: 250, h: 160 }, // RaspberryPi3Element: PI_WIDTH=250 PI_HEIGHT=160
'raspberry-pi-4': { w: 330, h: 215 }, // RaspberryPi4Element — real board photo (925×602 @ scale)
'raspberry-pi-5': { w: 330, h: 220 }, // RaspberryPi5Element — real board photo (1024×681 @ scale)
esp32: { w: 141, h: 265 }, // esp32-devkit-v1: 28.2 × 53 mm
'esp32-s3': { w: 128, h: 350 }, // esp32-s3-devkitc-1: 25.5 × 70 mm
'esp32-c3': { w: 127, h: 215 }, // esp32-c3-devkitm-1: 25.4 × 42.9 mm
'pi-pico-w': { w: 105, h: 264 },
'esp32-devkit-c-v4': { w: 140, h: 283 },
'esp32-cam': { w: 136, h: 202 },
'wemos-lolin32-lite': { w: 128, h: 250 },
'xiao-esp32-s3': { w: 91, h: 117 },
'arduino-nano-esp32': { w: 217, h: 90 },
'xiao-esp32-c3': { w: 91, h: 117 },
'aitewinrobot-esp32c3-supermini': { w: 90, h: 123 },
'stm32-bluepill': { w: 114, h: 271 }, // 22.855 × 54.193 mm (wokwi-boards SVG)
'stm32-blackpill': { w: 103, h: 266 }, // 20.695 × 53.125 mm (wokwi-boards SVG)
'stm32-bluepill-f103cb': { w: 114, h: 271 }, // reuses Blue Pill SVG
'stm32-blackpill-f401': { w: 103, h: 266 }, // reuses Black Pill SVG
// Inline-rendered boards — sizes match Stm32BoardElement.inlineConfig()
// (INLINE_W=158; h = 24 + rows*13 + 16).
'stm32-f4-discovery': { w: 158, h: 235 }, // 15 rows per side
'stm32-olimex-h405': { w: 158, h: 196 }, // 12 rows per side
'stm32-netduino-plus2': { w: 158, h: 196 }, // 12 rows per side
'stm32-netduino2': { w: 158, h: 196 }, // 12 rows per side
attiny85: { w: 160, h: 132 },
};
interface BoardOnCanvasProps {
board: BoardInstance;
running: boolean;
led13?: boolean;
isActive?: boolean;
/** When false, the pin overlay is hidden — keeps the canvas uncluttered when
* the user isn't hovering, isn't selecting, and isn't actively wiring. */
showPins?: boolean;
onMouseDown: (e: React.MouseEvent) => void;
onContextMenu?: (e: React.MouseEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onPinClick: (componentId: string, pinName: string, x: number, y: number) => void;
zoom?: number;
}
export const BoardOnCanvas = ({
board,
running,
led13 = false,
isActive = false,
showPins = true,
onMouseDown,
onContextMenu,
onMouseEnter,
onMouseLeave,
onPinClick,
zoom = 1,
}: BoardOnCanvasProps) => {
const { id, boardKind, x, y } = board;
const size = BOARD_SIZE[boardKind] ?? { w: 300, h: 200 };
// Status dot color: green=running, amber=compiled, gray=idle
const statusColor = board.running ? '#22c55e' : board.compiledProgram ? '#f59e0b' : '#6b7280';
const boardEl = (() => {
switch (boardKind) {
case 'arduino-uno':
return <ArduinoUno id={id} x={x} y={y} led13={led13} />;
case 'arduino-nano':
return <ArduinoNano id={id} x={x} y={y} led13={led13} />;
case 'arduino-mega':
return <ArduinoMega id={id} x={x} y={y} led13={led13} />;
// 'raspberry-pi-pico' used to render <NanoRP2040> (a wokwi-nano-
// rp2040-connect element with D2-D13 pin labels). That was a
// misnaming bug — the Nano RP2040 Connect is a different board.
// Use the same Pico Web Component as 'pi-pico-w' so the pins are
// labeled GP0..GP28, 3V3, VBUS, etc. — matching the FQBN
// (rp2040:rp2040:rpipico) and every Pi-Pico sketch's #defines.
case 'raspberry-pi-pico':
case 'pi-pico-w':
return <PiPicoW id={id} x={x} y={y} />;
case 'raspberry-pi-3':
return <RaspberryPi3 id={id} x={x} y={y} />;
case 'raspberry-pi-4':
return <RaspberryPi4 id={id} x={x} y={y} />;
case 'raspberry-pi-5':
return <RaspberryPi5 id={id} x={x} y={y} />;
case 'esp32':
case 'esp32-devkit-c-v4':
case 'esp32-cam':
case 'wemos-lolin32-lite':
case 'esp32-s3':
case 'xiao-esp32-s3':
case 'arduino-nano-esp32':
case 'esp32-c3':
case 'xiao-esp32-c3':
case 'aitewinrobot-esp32c3-supermini':
return <Esp32 id={id} x={x} y={y} boardKind={boardKind} />;
case 'stm32-bluepill':
return <Stm32BluePill id={id} x={x} y={y} />;
case 'stm32-blackpill':
return <Stm32BlackPill id={id} x={x} y={y} />;
case 'stm32-bluepill-f103cb':
return <Stm32BluePillF103CB id={id} x={x} y={y} />;
case 'stm32-blackpill-f401':
return <Stm32BlackPillF401 id={id} x={x} y={y} />;
case 'stm32-f4-discovery':
return <Stm32F4Discovery id={id} x={x} y={y} />;
case 'stm32-olimex-h405':
return <Stm32OlimexH405 id={id} x={x} y={y} />;
case 'stm32-netduino-plus2':
return <Stm32NetduinoPlus2 id={id} x={x} y={y} />;
case 'stm32-netduino2':
return <Stm32Netduino2 id={id} x={x} y={y} />;
case 'attiny85':
return <Attiny85 id={id} x={x} y={y} led1={led13} />;
}
})();
return (
<>
{boardEl}
{/* Active board highlight ring */}
{isActive && (
<div
style={{
position: 'absolute',
left: x - 3,
top: y - 3,
width: size.w + 6,
height: size.h + 6,
border: '2px solid #007acc',
borderRadius: 6,
pointerEvents: 'none',
zIndex: 2,
}}
/>
)}
{/* Status dot — top-right corner */}
<div
style={{
position: 'absolute',
left: x + size.w - 10,
top: y - 6,
width: 12,
height: 12,
borderRadius: '50%',
background: statusColor,
border: '2px solid #1e1e1e',
pointerEvents: 'none',
zIndex: 10,
transition: 'background 0.3s',
}}
title={board.running ? 'Running' : board.compiledProgram ? 'Compiled' : 'Idle'}
/>
{/* Drag overlay — hidden during simulation */}
{!running && (
<div
data-board-overlay="true"
data-board-id={id}
style={{
position: 'absolute',
left: x,
top: y,
width: size.w,
height: size.h,
cursor: 'move',
zIndex: 1,
}}
onMouseDown={(e) => {
e.stopPropagation();
onMouseDown(e);
}}
onContextMenu={onContextMenu}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
)}
{/* Pin overlay for wire connections */}
<PinOverlay
componentId={id}
componentX={x}
componentY={y}
onPinClick={onPinClick}
showPins={showPins}
wrapperOffsetX={0}
wrapperOffsetY={0}
zoom={zoom}
/>
</>
);
};