feat: enhance oscilloscope and simulator functionality
- Added `onPinStateChange` method to `ChipParts` for handling pin state changes in 7-segment displays. - Updated `useOscilloscopeStore` to allow independent monitoring of multiple boards by adding `boardId` to channels and modifying `addChannel` method. - Modified `getOscilloscopeCallback` in `useSimulatorStore` to filter channels based on `boardId` and pin number. - Adjusted component and board position calculations in `useSimulatorStore` to account for wrapper offsets. - Updated submodule references for `qemu-lcgamboa`, `rp2040js`, and `wokwi-elements` to indicate dirty states.pull/47/head
parent
5e6c5d451b
commit
bc31ee76c1
|
|
@ -29,33 +29,33 @@ const PINS_ESP32 = [
|
|||
{ name: 'EN', x: 6, y: 29 },
|
||||
{ name: 'VN', x: 6, y: 42 },
|
||||
{ name: 'VP', x: 6, y: 54 },
|
||||
{ name: 'D34', x: 6, y: 67 },
|
||||
{ name: 'D35', x: 6, y: 80 },
|
||||
{ name: 'D32', x: 6, y: 93 },
|
||||
{ name: 'D33', x: 6, y: 105 },
|
||||
{ name: 'D25', x: 6, y: 118 },
|
||||
{ name: 'D26', x: 6, y: 131 },
|
||||
{ name: 'D27', x: 6, y: 143 },
|
||||
{ name: 'D14', x: 6, y: 156 },
|
||||
{ name: 'D12', x: 6, y: 169 },
|
||||
{ name: 'D13', x: 6, y: 181 },
|
||||
{ name: '34', x: 6, y: 67 },
|
||||
{ name: '35', x: 6, y: 80 },
|
||||
{ name: '32', x: 6, y: 93 },
|
||||
{ name: '33', x: 6, y: 105 },
|
||||
{ name: '25', x: 6, y: 118 },
|
||||
{ name: '26', x: 6, y: 131 },
|
||||
{ name: '27', x: 6, y: 143 },
|
||||
{ name: '14', x: 6, y: 156 },
|
||||
{ name: '12', x: 6, y: 169 },
|
||||
{ name: '13', x: 6, y: 181 },
|
||||
{ name: 'GND', x: 6, y: 194 },
|
||||
{ name: 'VIN', x: 6, y: 207 },
|
||||
{ name: '3V3', x: 134, y: 207 },
|
||||
{ name: 'GND', x: 134, y: 194 },
|
||||
{ name: 'D15', x: 134, y: 181 },
|
||||
{ name: 'D2', x: 134, y: 169 },
|
||||
{ name: 'D4', x: 134, y: 156 },
|
||||
{ name: 'GND2', x: 134, y: 194 },
|
||||
{ name: '15', x: 134, y: 181 },
|
||||
{ name: '2', x: 134, y: 169 },
|
||||
{ name: '4', x: 134, y: 156 },
|
||||
{ name: 'RX2', x: 134, y: 143 },
|
||||
{ name: 'TX2', x: 134, y: 131 },
|
||||
{ name: 'D5', x: 134, y: 118 },
|
||||
{ name: 'D18', x: 134, y: 105 },
|
||||
{ name: 'D19', x: 134, y: 93 },
|
||||
{ name: 'D21', x: 134, y: 80 },
|
||||
{ name: '5', x: 134, y: 118 },
|
||||
{ name: '18', x: 134, y: 105 },
|
||||
{ name: '19', x: 134, y: 93 },
|
||||
{ name: '21', x: 134, y: 80 },
|
||||
{ name: 'RX0', x: 134, y: 67 },
|
||||
{ name: 'TX0', x: 134, y: 54 },
|
||||
{ name: 'D22', x: 134, y: 42 },
|
||||
{ name: 'D23', x: 134, y: 29 },
|
||||
{ name: '22', x: 134, y: 42 },
|
||||
{ name: '23', x: 134, y: 29 },
|
||||
];
|
||||
|
||||
// ESP32-S3 DevKitC-1: 25.527 mm × 70.057 mm → 128 × 350 px
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Oscilloscope / Logic Analyzer panel.
|
||||
*
|
||||
* Captures digital pin HIGH/LOW transitions (with timestamps from the CPU
|
||||
* cycle counter) and renders them as step waveforms on a <canvas>.
|
||||
* Supports multiple boards: each channel is tied to a specific (boardId, pin)
|
||||
* pair, so D13 on board A and D13 on board B are tracked independently.
|
||||
*
|
||||
* Usage:
|
||||
* - Click "+ Add Channel" to pick pins to monitor.
|
||||
* - Click "+ Add Channel" → choose a board → choose a pin.
|
||||
* - Adjust Time/div to zoom in or out.
|
||||
* - Click Run / Pause to freeze the display without stopping the simulation.
|
||||
* - Click Clear to wipe all captured samples.
|
||||
|
|
@ -18,8 +18,11 @@ import React, {
|
|||
useCallback,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useOscilloscopeStore, type OscChannel, type OscSample } from '../../store/useOscilloscopeStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import './Oscilloscope.css';
|
||||
|
||||
// Horizontal divisions shown at once
|
||||
|
|
@ -37,39 +40,47 @@ const TIME_DIV_OPTIONS: { label: string; ms: number }[] = [
|
|||
{ label: '500 ms', ms: 500 },
|
||||
];
|
||||
|
||||
/** Possible digital pins to monitor (Uno/Nano layout) */
|
||||
const AVAILABLE_PINS = [
|
||||
{ pin: 0, label: 'D0' },
|
||||
{ pin: 1, label: 'D1' },
|
||||
{ pin: 2, label: 'D2' },
|
||||
{ pin: 3, label: 'D3' },
|
||||
{ pin: 4, label: 'D4' },
|
||||
{ pin: 5, label: 'D5' },
|
||||
{ pin: 6, label: 'D6' },
|
||||
{ pin: 7, label: 'D7' },
|
||||
{ pin: 8, label: 'D8' },
|
||||
{ pin: 9, label: 'D9' },
|
||||
{ pin: 10, label: 'D10' },
|
||||
{ pin: 11, label: 'D11' },
|
||||
{ pin: 12, label: 'D12' },
|
||||
{ pin: 13, label: 'D13' },
|
||||
{ pin: 14, label: 'A0' },
|
||||
{ pin: 15, label: 'A1' },
|
||||
{ pin: 16, label: 'A2' },
|
||||
{ pin: 17, label: 'A3' },
|
||||
{ pin: 18, label: 'A4' },
|
||||
{ pin: 19, label: 'A5' },
|
||||
];
|
||||
/** Return the list of monitorable pins for a given board kind */
|
||||
function getPinsForBoardKind(boardKind: BoardKind): { pin: number; label: string }[] {
|
||||
switch (boardKind) {
|
||||
case 'arduino-mega':
|
||||
return [
|
||||
...Array.from({ length: 54 }, (_, i) => ({ pin: i, label: `D${i}` })),
|
||||
...Array.from({ length: 16 }, (_, i) => ({ pin: 54 + i, label: `A${i}` })),
|
||||
];
|
||||
case 'attiny85':
|
||||
return Array.from({ length: 6 }, (_, i) => ({ pin: i, label: `D${i}` }));
|
||||
case 'raspberry-pi-pico':
|
||||
case 'pi-pico-w':
|
||||
return Array.from({ length: 29 }, (_, i) => ({ pin: i, label: `GP${i}` }));
|
||||
case 'esp32':
|
||||
case 'esp32-devkit-c-v4':
|
||||
case 'esp32-cam':
|
||||
case 'wemos-lolin32-lite':
|
||||
return Array.from({ length: 40 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
|
||||
case 'esp32-s3':
|
||||
case 'xiao-esp32-s3':
|
||||
case 'arduino-nano-esp32':
|
||||
return Array.from({ length: 45 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
|
||||
case 'esp32-c3':
|
||||
case 'xiao-esp32-c3':
|
||||
case 'aitewinrobot-esp32c3-supermini':
|
||||
return Array.from({ length: 22 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
|
||||
case 'riscv-generic':
|
||||
return Array.from({ length: 20 }, (_, i) => ({ pin: i, label: `D${i}` }));
|
||||
case 'raspberry-pi-3':
|
||||
return Array.from({ length: 28 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
|
||||
default:
|
||||
// arduino-uno, arduino-nano
|
||||
return [
|
||||
...Array.from({ length: 14 }, (_, i) => ({ pin: i, label: `D${i}` })),
|
||||
...Array.from({ length: 6 }, (_, i) => ({ pin: 14 + i, label: `A${i}` })),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Canvas rendering helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Draw a single channel's waveform onto `canvas`.
|
||||
* @param samples The sample ring-buffer for this channel.
|
||||
* @param color Stroke color (CSS string).
|
||||
* @param windowEndMs Right edge of the time window.
|
||||
* @param windowMs Total time window width (NUM_DIVS * timeDivMs).
|
||||
*/
|
||||
function drawWaveform(
|
||||
canvas: HTMLCanvasElement,
|
||||
samples: OscSample[],
|
||||
|
|
@ -105,10 +116,7 @@ function drawWaveform(
|
|||
if (samples.length === 0) return;
|
||||
|
||||
const windowStartMs = windowEndMs - windowMs;
|
||||
|
||||
// Convert timeMs → canvas x pixel
|
||||
const toX = (t: number) => ((t - windowStartMs) / windowMs) * width;
|
||||
|
||||
const HIGH_Y = Math.round(height * 0.15);
|
||||
const LOW_Y = Math.round(height * 0.85);
|
||||
|
||||
|
|
@ -116,7 +124,6 @@ function drawWaveform(
|
|||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
|
||||
// Find the last sample before the window to establish the initial state
|
||||
let initState = false;
|
||||
for (let i = samples.length - 1; i >= 0; i--) {
|
||||
if (samples[i].timeMs <= windowStartMs) {
|
||||
|
|
@ -134,27 +141,20 @@ function drawWaveform(
|
|||
|
||||
const x = Math.max(0, Math.min(width, toX(s.timeMs)));
|
||||
const nextY = s.state ? HIGH_Y : LOW_Y;
|
||||
|
||||
// Vertical step
|
||||
ctx.lineTo(x, currentY);
|
||||
ctx.lineTo(x, nextY);
|
||||
currentY = nextY;
|
||||
}
|
||||
|
||||
// Extend to the right edge
|
||||
ctx.lineTo(width, currentY);
|
||||
ctx.stroke();
|
||||
|
||||
// HIGH / LOW labels at the right margin
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = '9px monospace';
|
||||
ctx.fillText('H', width - 12, HIGH_Y + 3);
|
||||
ctx.fillText('L', width - 12, LOW_Y + 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the time-axis ruler below all the channels.
|
||||
*/
|
||||
function drawRuler(
|
||||
canvas: HTMLCanvasElement,
|
||||
windowEndMs: number,
|
||||
|
|
@ -182,7 +182,6 @@ function drawRuler(
|
|||
ctx.lineTo(x, 5);
|
||||
ctx.stroke();
|
||||
|
||||
// Format the label
|
||||
const absMs = Math.abs(timeAtDiv);
|
||||
const label = absMs >= 1000
|
||||
? `${(timeAtDiv / 1000).toFixed(1)}s`
|
||||
|
|
@ -194,7 +193,7 @@ function drawRuler(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Channel canvas hook ─────────────────────────────────────────────────────
|
||||
// ── Channel canvas ──────────────────────────────────────────────────────────
|
||||
|
||||
interface ChannelCanvasProps {
|
||||
channel: OscChannel;
|
||||
|
|
@ -204,15 +203,11 @@ interface ChannelCanvasProps {
|
|||
}
|
||||
|
||||
const ChannelCanvas: React.FC<ChannelCanvasProps> = ({
|
||||
channel,
|
||||
samples,
|
||||
windowEndMs,
|
||||
windowMs,
|
||||
channel, samples, windowEndMs, windowMs,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Re-draw whenever data or sizing changes
|
||||
useLayoutEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const wrap = wrapRef.current;
|
||||
|
|
@ -221,13 +216,11 @@ const ChannelCanvas: React.FC<ChannelCanvasProps> = ({
|
|||
const { width, height } = wrap.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) return;
|
||||
|
||||
canvas.width = Math.floor(width) * window.devicePixelRatio;
|
||||
canvas.width = Math.floor(width) * window.devicePixelRatio;
|
||||
canvas.height = Math.floor(height) * window.devicePixelRatio;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
drawWaveform(canvas, samples, channel.color, windowEndMs, windowMs);
|
||||
// Intentionally exclude canvasRef/wrapRef (stable refs) and the
|
||||
// module-level drawWaveform function from the dependency array.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [samples, channel.color, windowEndMs, windowMs]);
|
||||
|
||||
|
|
@ -258,12 +251,11 @@ const RulerCanvas: React.FC<RulerCanvasProps> = ({ windowEndMs, windowMs, timeDi
|
|||
const { width, height } = wrap.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) return;
|
||||
|
||||
canvas.width = Math.floor(width) * window.devicePixelRatio;
|
||||
canvas.width = Math.floor(width) * window.devicePixelRatio;
|
||||
canvas.height = Math.floor(height) * window.devicePixelRatio;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
drawRuler(canvas, windowEndMs, windowMs, timeDivMs);
|
||||
// Intentionally exclude stable canvasRef/wrapRef and module-level drawRuler.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowEndMs, windowMs, timeDivMs]);
|
||||
|
||||
|
|
@ -274,6 +266,93 @@ const RulerCanvas: React.FC<RulerCanvasProps> = ({ windowEndMs, windowMs, timeDi
|
|||
);
|
||||
};
|
||||
|
||||
// ── Channel picker (two-step: board → pin) ───────────────────────────────────
|
||||
|
||||
interface ChannelPickerProps {
|
||||
onAdd: (boardId: string, pin: number, pinLabel: string) => void;
|
||||
activeChannels: OscChannel[];
|
||||
onClose: () => void;
|
||||
anchorRect: DOMRect;
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const ChannelPicker: React.FC<ChannelPickerProps> = ({
|
||||
onAdd, activeChannels, onClose, anchorRect, dropdownRef,
|
||||
}) => {
|
||||
const boards = useSimulatorStore((s) => s.boards);
|
||||
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
||||
const [selectedBoardId, setSelectedBoardId] = useState<string>(
|
||||
activeBoardId ?? boards[0]?.id ?? '',
|
||||
);
|
||||
|
||||
const selectedBoard = boards.find((b) => b.id === selectedBoardId) ?? boards[0];
|
||||
const pins = selectedBoard ? getPinsForBoardKind(selectedBoard.boardKind) : [];
|
||||
|
||||
const activePinsForBoard = new Set(
|
||||
activeChannels.filter((c) => c.boardId === selectedBoardId).map((c) => c.pin),
|
||||
);
|
||||
|
||||
// Open upward from the anchor button, fixed in the viewport
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
left: anchorRect.left,
|
||||
bottom: window.innerHeight - anchorRect.top + 4,
|
||||
zIndex: 9999,
|
||||
};
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="osc-picker-dropdown osc-picker-multiboard"
|
||||
style={style}
|
||||
>
|
||||
{/* Board tabs */}
|
||||
<div className="osc-picker-board-tabs">
|
||||
{boards.map((b) => (
|
||||
<button
|
||||
key={b.id}
|
||||
className={`osc-picker-board-tab${b.id === selectedBoard?.id ? ' active' : ''}`}
|
||||
onClick={() => setSelectedBoardId(b.id)}
|
||||
title={BOARD_KIND_LABELS[b.boardKind]}
|
||||
>
|
||||
{b.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Board label */}
|
||||
{selectedBoard && (
|
||||
<div className="osc-picker-board-label">
|
||||
{BOARD_KIND_LABELS[selectedBoard.boardKind]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pin grid */}
|
||||
<div className="osc-picker-pins">
|
||||
{pins.map(({ pin, label }) => {
|
||||
const added = activePinsForBoard.has(pin);
|
||||
return (
|
||||
<button
|
||||
key={pin}
|
||||
className={`osc-pin-btn${added ? ' osc-pin-btn-active' : ''}`}
|
||||
onClick={() => {
|
||||
if (!added) {
|
||||
onAdd(selectedBoardId, pin, label);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={added ? 'Already added' : `Monitor ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
// ── Main component ─────────────────────────────────────────────────────────
|
||||
|
||||
export const Oscilloscope: React.FC = () => {
|
||||
|
|
@ -289,34 +368,47 @@ export const Oscilloscope: React.FC = () => {
|
|||
clearSamples,
|
||||
} = useOscilloscopeStore();
|
||||
|
||||
const simRunning = useSimulatorStore((s) => s.running);
|
||||
// Any board running → oscilloscope can capture
|
||||
const anyRunning = useSimulatorStore((s) => s.boards.some((b) => b.running));
|
||||
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
const [pickerAnchor, setPickerAnchor] = useState<DOMRect | null>(null);
|
||||
const addBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close picker when clicking outside
|
||||
const handleTogglePicker = () => {
|
||||
if (showPicker) {
|
||||
setShowPicker(false);
|
||||
setPickerAnchor(null);
|
||||
} else {
|
||||
const rect = addBtnRef.current?.getBoundingClientRect() ?? null;
|
||||
setPickerAnchor(rect);
|
||||
setShowPicker(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Close picker on outside click (checks both the button and the portal dropdown)
|
||||
useEffect(() => {
|
||||
if (!showPicker) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) {
|
||||
const target = e.target as Node;
|
||||
const inBtn = addBtnRef.current?.contains(target);
|
||||
const inDropdown = dropdownRef.current?.contains(target);
|
||||
if (!inBtn && !inDropdown) {
|
||||
setShowPicker(false);
|
||||
setPickerAnchor(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [showPicker]);
|
||||
|
||||
// ── Compute the display window ──────────────────────────────────────────
|
||||
//
|
||||
// We show the last (NUM_DIVS * timeDivMs) ms of captured data.
|
||||
// windowEndMs = latest sample time across all channels (or 0 if none).
|
||||
|
||||
// ── Display window ──────────────────────────────────────────────────────
|
||||
const [, forceRedraw] = useState(0);
|
||||
|
||||
// Poll at 60 fps while simulation + capture are running
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (simRunning && capturing) {
|
||||
if (anyRunning && capturing) {
|
||||
const tick = () => {
|
||||
forceRedraw((n) => n + 1);
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
|
|
@ -326,11 +418,10 @@ export const Oscilloscope: React.FC = () => {
|
|||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}
|
||||
}, [simRunning, capturing]);
|
||||
}, [anyRunning, capturing]);
|
||||
|
||||
const windowMs = NUM_DIVS * timeDivMs;
|
||||
|
||||
// Find the latest sample time to anchor the right edge of the window
|
||||
let windowEndMs = 0;
|
||||
for (const ch of channels) {
|
||||
const buf = samples[ch.id] ?? [];
|
||||
|
|
@ -338,15 +429,23 @@ export const Oscilloscope: React.FC = () => {
|
|||
windowEndMs = Math.max(windowEndMs, buf[buf.length - 1].timeMs);
|
||||
}
|
||||
}
|
||||
// Always advance at least one window ahead so the display isn't stale
|
||||
windowEndMs = Math.max(windowEndMs, windowMs);
|
||||
|
||||
const handleAddChannel = useCallback((pin: number) => {
|
||||
addChannel(pin);
|
||||
setShowPicker(false);
|
||||
const handleAddChannel = useCallback((boardId: string, pin: number, pinLabel: string) => {
|
||||
addChannel(boardId, pin, pinLabel);
|
||||
}, [addChannel]);
|
||||
|
||||
const activePins = new Set(channels.map((c) => c.pin));
|
||||
// Short display name for a board id — strip leading "arduino-", "raspberry-pi-", etc.
|
||||
const boardShortName = (boardId: string) => {
|
||||
const parts = boardId.split('-');
|
||||
// If numeric suffix like "arduino-uno-2", keep the suffix
|
||||
const last = parts[parts.length - 1];
|
||||
const isNum = /^\d+$/.test(last);
|
||||
if (isNum && parts.length >= 2) {
|
||||
return `${parts[parts.length - 2]}-${last}`;
|
||||
}
|
||||
return last;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="osc-container">
|
||||
|
|
@ -354,31 +453,25 @@ export const Oscilloscope: React.FC = () => {
|
|||
<div className="osc-header">
|
||||
<span className="osc-title">Oscilloscope</span>
|
||||
|
||||
{/* Add Channel button + dropdown */}
|
||||
<div className="osc-picker-wrap" ref={pickerRef}>
|
||||
<button
|
||||
className="osc-btn"
|
||||
onClick={() => setShowPicker((v) => !v)}
|
||||
title="Add a pin channel"
|
||||
>
|
||||
+ Add Channel
|
||||
</button>
|
||||
{/* Add Channel button + portal picker */}
|
||||
<button
|
||||
ref={addBtnRef}
|
||||
className="osc-btn"
|
||||
onClick={handleTogglePicker}
|
||||
title="Add a pin channel"
|
||||
>
|
||||
+ Add Channel
|
||||
</button>
|
||||
|
||||
{showPicker && (
|
||||
<div className="osc-picker-dropdown">
|
||||
{AVAILABLE_PINS.map(({ pin, label }) => (
|
||||
<button
|
||||
key={pin}
|
||||
className={`osc-pin-btn${activePins.has(pin) ? ' osc-pin-btn-active' : ''}`}
|
||||
onClick={() => !activePins.has(pin) && handleAddChannel(pin)}
|
||||
title={activePins.has(pin) ? 'Already added' : `Monitor ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showPicker && pickerAnchor && (
|
||||
<ChannelPicker
|
||||
onAdd={handleAddChannel}
|
||||
activeChannels={channels}
|
||||
onClose={() => { setShowPicker(false); setPickerAnchor(null); }}
|
||||
anchorRect={pickerAnchor}
|
||||
dropdownRef={dropdownRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Time / div */}
|
||||
<span className="osc-label">Time/div:</span>
|
||||
|
|
@ -422,8 +515,10 @@ export const Oscilloscope: React.FC = () => {
|
|||
<div className="osc-waveforms">
|
||||
{channels.map((ch) => (
|
||||
<div key={ch.id} className="osc-channel-row">
|
||||
{/* Label + remove button */}
|
||||
<div className="osc-channel-label">
|
||||
<span className="osc-channel-board" title={ch.boardId}>
|
||||
{boardShortName(ch.boardId)}
|
||||
</span>
|
||||
<span className="osc-channel-name" style={{ color: ch.color }}>
|
||||
{ch.label}
|
||||
</span>
|
||||
|
|
@ -436,7 +531,6 @@ export const Oscilloscope: React.FC = () => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Waveform canvas */}
|
||||
<ChannelCanvas
|
||||
channel={ch}
|
||||
samples={samples[ch.id] ?? []}
|
||||
|
|
@ -447,7 +541,6 @@ export const Oscilloscope: React.FC = () => {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Time ruler */}
|
||||
<RulerCanvas
|
||||
windowEndMs={windowEndMs}
|
||||
windowMs={windowMs}
|
||||
|
|
|
|||
|
|
@ -469,7 +469,7 @@ export const SimulatorCanvas = () => {
|
|||
subscribeComponentToPin(component, component.properties.pin as number, 'A');
|
||||
} else {
|
||||
// 2. Subscribe by finding wires connected to arduino
|
||||
const connectedWires = useSimulatorStore.getState().wires.filter(
|
||||
const connectedWires = wires.filter(
|
||||
w => w.start.componentId === component.id || w.end.componentId === component.id
|
||||
);
|
||||
|
||||
|
|
@ -502,7 +502,7 @@ export const SimulatorCanvas = () => {
|
|||
return () => {
|
||||
unsubscribers.forEach(unsub => unsub());
|
||||
};
|
||||
}, [components, pinManager, updateComponentState]);
|
||||
}, [components, wires, boards, pinManager, updateComponentState]);
|
||||
|
||||
// Handle keyboard delete
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -170,6 +170,9 @@ void loop() {
|
|||
end: { componentId: 'led-green', pinName: 'A' },
|
||||
color: '#00ff00',
|
||||
},
|
||||
{ id: 'wire-red-gnd', start: { componentId: 'led-red', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-yellow-gnd', start: { componentId: 'led-yellow', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-green-gnd', start: { componentId: 'led-green', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -234,6 +237,8 @@ void loop() {
|
|||
end: { componentId: 'led-1', pinName: 'A' },
|
||||
color: '#ff0000',
|
||||
},
|
||||
{ id: 'wire-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-button-gnd', start: { componentId: 'button-1', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -288,6 +293,7 @@ void loop() {
|
|||
end: { componentId: 'led-1', pinName: 'A' },
|
||||
color: '#0000ff',
|
||||
},
|
||||
{ id: 'wire-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -411,6 +417,7 @@ void loop() {
|
|||
end: { componentId: 'rgb-led-1', pinName: 'B' },
|
||||
color: '#0000ff',
|
||||
},
|
||||
{ id: 'wire-rgb-gnd', start: { componentId: 'rgb-led-1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -614,6 +621,14 @@ void loop() {
|
|||
end: { componentId: 'button-yellow', pinName: '1.l' },
|
||||
color: '#00aaff',
|
||||
},
|
||||
{ id: 'wire-led-red-gnd', start: { componentId: 'led-red', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-led-green-gnd', start: { componentId: 'led-green', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-led-blue-gnd', start: { componentId: 'led-blue', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-led-yellow-gnd', start: { componentId: 'led-yellow', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-btn-red-gnd', start: { componentId: 'button-red', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-btn-green-gnd', start: { componentId: 'button-green', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-btn-blue-gnd', start: { componentId: 'button-blue', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'wire-btn-yellow-gnd', start: { componentId: 'button-yellow', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -911,6 +926,7 @@ void loop() {
|
|||
],
|
||||
wires: [
|
||||
{ id: 'w-led', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'led-1', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -1411,6 +1427,7 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-blink', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w2', start: { componentId: 'led-blink', pinName: 'C' }, end: { componentId: 'r1', pinName: '1' }, color: '#999999' },
|
||||
{ id: 'w3', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1456,6 +1473,7 @@ void loop() {
|
|||
],
|
||||
wires: [
|
||||
{ id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'TX' }, end: { componentId: 'led-rx', pinName: 'A' }, color: '#ff8800' },
|
||||
{ id: 'w2', start: { componentId: 'led-rx', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1510,6 +1528,7 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-status', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w2', start: { componentId: 'led-status', pinName: 'C' }, end: { componentId: 'r1', pinName: '1' }, color: '#999999' },
|
||||
{ id: 'w3', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1575,6 +1594,8 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-scan', pinName: 'A' }, color: '#4488ff' },
|
||||
{ id: 'w2', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-found', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w3', start: { componentId: 'led-scan', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w4', start: { componentId: 'led-found', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1645,6 +1666,8 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w-sda', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-i2c', pinName: 'A' }, color: '#4488ff' },
|
||||
{ id: 'w-scl', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-rtc', pinName: 'A' }, color: '#ffaa00' },
|
||||
{ id: 'w-sda-gnd', start: { componentId: 'led-i2c', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-scl-gnd', start: { componentId: 'led-rtc', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1733,6 +1756,8 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w-sda', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-write', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'w-scl', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-read', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w-write-gnd', start: { componentId: 'led-write', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-read-gnd', start: { componentId: 'led-read', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1794,6 +1819,9 @@ void loop() {
|
|||
{ id: 'w-mosi', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'led-mosi', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'w-miso', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'led-miso', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w-sck', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'led-sck', pinName: 'A' }, color: '#ffaa00' },
|
||||
{ id: 'w-mosi-gnd', start: { componentId: 'led-mosi', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-miso-gnd', start: { componentId: 'led-miso', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-sck-gnd', start: { componentId: 'led-sck', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1846,6 +1874,11 @@ void loop() {
|
|||
{ id: 'w-a0', start: { componentId: 'nano-rp2040', pinName: 'A0' }, end: { componentId: 'pot-a0', pinName: 'SIG' }, color: '#4488ff' },
|
||||
{ id: 'w-a1', start: { componentId: 'nano-rp2040', pinName: 'A1' }, end: { componentId: 'pot-a1', pinName: 'SIG' }, color: '#44cc44' },
|
||||
{ id: 'w-temp', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-temp', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'w-pot-a0-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-a0', pinName: 'VCC' }, color: '#ff0000' },
|
||||
{ id: 'w-pot-a0-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-a0', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w-pot-a1-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-a1', pinName: 'VCC' }, color: '#ff0000' },
|
||||
{ id: 'w-pot-a1-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-a1', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w-temp-gnd', start: { componentId: 'led-temp', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -1976,6 +2009,11 @@ void loop() {
|
|||
{ id: 'w-spi', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'led-spi', pinName: 'A' }, color: '#ffaa00' },
|
||||
{ id: 'w-adc', start: { componentId: 'nano-rp2040', pinName: 'A0' }, end: { componentId: 'pot-adc', pinName: 'SIG' }, color: '#cc44cc' },
|
||||
{ id: 'w-gpio', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-gpio', pinName: 'A' }, color: '#00cc00' },
|
||||
{ id: 'w-i2c-gnd', start: { componentId: 'led-i2c', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-spi-gnd', start: { componentId: 'led-spi', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-gpio-gnd', start: { componentId: 'led-gpio', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'w-pot-adc-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-adc', pinName: 'VCC' }, color: '#ff0000' },
|
||||
{ id: 'w-pot-adc-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-adc', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
// ─── ESP32 Examples ───────────────────────────────────────────────────────
|
||||
|
|
@ -2015,10 +2053,10 @@ void loop() {
|
|||
{ type: 'wokwi-led', id: 'led-ext', x: 460, y: 190, properties: { color: 'red' } },
|
||||
],
|
||||
wires: [
|
||||
// GPIO4 → LED anode (direct — subscription system needs board→component wire)
|
||||
{ id: 'w-gpio4-led', start: { componentId: 'arduino-uno', pinName: 'GPIO4' }, end: { componentId: 'led-ext', pinName: 'A' }, color: '#e74c3c' },
|
||||
// GPIO4 → LED anode
|
||||
{ id: 'w-gpio4-led', start: { componentId: 'esp32', pinName: '4' }, end: { componentId: 'led-ext', pinName: 'A' }, color: '#e74c3c' },
|
||||
// LED cathode → GND
|
||||
{ id: 'w-gnd', start: { componentId: 'led-ext', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#2c3e50' },
|
||||
{ id: 'w-gnd', start: { componentId: 'led-ext', pinName: 'C' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#2c3e50' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2294,6 +2332,8 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w-btn', start: { componentId: 'arduino-uno', pinName: '2' }, end: { componentId: 'btn1', pinName: '1a' }, color: '#00aaff' },
|
||||
{ id: 'w-led', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'led1', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'w-led-gnd', start: { componentId: 'led1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w-btn-gnd', start: { componentId: 'btn1', pinName: '1b' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2333,6 +2373,7 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'w-fade', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'led-fade', pinName: 'A' }, color: '#2244ff' },
|
||||
{ id: 'w-fade-r', start: { componentId: 'led-fade', pinName: 'C' }, end: { componentId: 'r-fade', pinName: '1' }, color: '#888888' },
|
||||
{ id: 'w-fade-gnd', start: { componentId: 'r-fade', pinName: '2' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -2452,6 +2493,14 @@ void loop() {
|
|||
{ id: 'w7', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'led7', pinName: 'A' }, color: '#aa44ff' },
|
||||
{ id: 'w8', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'led8', pinName: 'A' }, color: '#ffffff' },
|
||||
{ id: 'w9', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'led9', pinName: 'A' }, color: '#ff2222' },
|
||||
{ id: 'w2-gnd', start: { componentId: 'led2', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w3-gnd', start: { componentId: 'led3', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w4-gnd', start: { componentId: 'led4', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w5-gnd', start: { componentId: 'led5', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w6-gnd', start: { componentId: 'led6', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w7-gnd', start: { componentId: 'led7', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w8-gnd', start: { componentId: 'led8', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'w9-gnd', start: { componentId: 'led9', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2517,6 +2566,14 @@ void loop() {
|
|||
{ id: 'mw7', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'mled7', pinName: 'A' }, color: '#aa44ff' },
|
||||
{ id: 'mw8', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'mled8', pinName: 'A' }, color: '#ffffff' },
|
||||
{ id: 'mw9', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'mled9', pinName: 'A' }, color: '#ff2222' },
|
||||
{ id: 'mw2-gnd', start: { componentId: 'mled2', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw3-gnd', start: { componentId: 'mled3', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw4-gnd', start: { componentId: 'mled4', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw5-gnd', start: { componentId: 'mled5', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw6-gnd', start: { componentId: 'mled6', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw7-gnd', start: { componentId: 'mled7', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw8-gnd', start: { componentId: 'mled8', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'mw9-gnd', start: { componentId: 'mled9', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -2555,6 +2612,7 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'c3w1', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-led1', pinName: 'A' }, color: '#22cc22' },
|
||||
{ id: 'c3w2', start: { componentId: 'c3-led1', pinName: 'C' }, end: { componentId: 'c3-r1', pinName: '1' }, color: '#888888' },
|
||||
{ id: 'c3w3', start: { componentId: 'c3-r1', pinName: '2' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2636,6 +2694,7 @@ void loop() {
|
|||
{ id: 'c3-rw1', start: { componentId: 'esp32-c3', pinName: '6' }, end: { componentId: 'c3-rgb1', pinName: 'R' }, color: '#ff2222' },
|
||||
{ id: 'c3-rw2', start: { componentId: 'esp32-c3', pinName: '7' }, end: { componentId: 'c3-rgb1', pinName: 'G' }, color: '#22cc22' },
|
||||
{ id: 'c3-rw3', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-rgb1', pinName: 'B' }, color: '#2244ff' },
|
||||
{ id: 'c3-rw4', start: { componentId: 'c3-rgb1', pinName: 'COM' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2673,6 +2732,8 @@ void loop() {
|
|||
wires: [
|
||||
{ id: 'c3-bw1', start: { componentId: 'esp32-c3', pinName: '9' }, end: { componentId: 'c3-btn1', pinName: '1a' }, color: '#00aaff' },
|
||||
{ id: 'c3-bw2', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-led-btn', pinName: 'A' }, color: '#2244ff' },
|
||||
{ id: 'c3-bw3', start: { componentId: 'c3-led-btn', pinName: 'C' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' },
|
||||
{ id: 'c3-bw4', start: { componentId: 'c3-btn1', pinName: '1b' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2765,6 +2826,7 @@ void loop() {
|
|||
{ id: 'seg-e', start: { componentId: 'arduino-uno', pinName: '6' }, end: { componentId: 'seg1', pinName: 'E' }, color: '#4488ff' },
|
||||
{ id: 'seg-f', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'seg1', pinName: 'F' }, color: '#aa44ff' },
|
||||
{ id: 'seg-g', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'seg1', pinName: 'G' }, color: '#ffffff' },
|
||||
{ id: 'seg-gnd', start: { componentId: 'seg1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2816,27 +2878,28 @@ void loop() {
|
|||
{ type: 'wokwi-7segment', id: 'pico-seg1', x: 440, y: 140, properties: { common: 'cathode', color: 'green' } },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'ps-a', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'pico-seg1', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'ps-b', start: { componentId: 'nano-rp2040', pinName: 'D3' }, end: { componentId: 'pico-seg1', pinName: 'B' }, color: '#ff8800' },
|
||||
{ id: 'ps-c', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'pico-seg1', pinName: 'C' }, color: '#ffcc00' },
|
||||
{ id: 'ps-d', start: { componentId: 'nano-rp2040', pinName: 'D5' }, end: { componentId: 'pico-seg1', pinName: 'D' }, color: '#44cc44' },
|
||||
{ id: 'ps-e', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'pico-seg1', pinName: 'E' }, color: '#4488ff' },
|
||||
{ id: 'ps-f', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'pico-seg1', pinName: 'F' }, color: '#aa44ff' },
|
||||
{ id: 'ps-g', start: { componentId: 'nano-rp2040', pinName: 'D8' }, end: { componentId: 'pico-seg1', pinName: 'G' }, color: '#ffffff' },
|
||||
{ id: 'ps-a', start: { componentId: 'nano-rp2040', pinName: 'GP2' }, end: { componentId: 'pico-seg1', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'ps-b', start: { componentId: 'nano-rp2040', pinName: 'GP3' }, end: { componentId: 'pico-seg1', pinName: 'B' }, color: '#ff8800' },
|
||||
{ id: 'ps-c', start: { componentId: 'nano-rp2040', pinName: 'GP4' }, end: { componentId: 'pico-seg1', pinName: 'C' }, color: '#ffcc00' },
|
||||
{ id: 'ps-d', start: { componentId: 'nano-rp2040', pinName: 'GP5' }, end: { componentId: 'pico-seg1', pinName: 'D' }, color: '#44cc44' },
|
||||
{ id: 'ps-e', start: { componentId: 'nano-rp2040', pinName: 'GP6' }, end: { componentId: 'pico-seg1', pinName: 'E' }, color: '#4488ff' },
|
||||
{ id: 'ps-f', start: { componentId: 'nano-rp2040', pinName: 'GP7' }, end: { componentId: 'pico-seg1', pinName: 'F' }, color: '#aa44ff' },
|
||||
{ id: 'ps-g', start: { componentId: 'nano-rp2040', pinName: 'GP8' }, end: { componentId: 'pico-seg1', pinName: 'G' }, color: '#ffffff' },
|
||||
{ id: 'ps-gnd', start: { componentId: 'pico-seg1', pinName: 'COM' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'esp32-7segment',
|
||||
title: 'ESP32: 7-Segment Counter',
|
||||
description: 'Count 0–9 on a 7-segment display driven from GPIO 12–18 on the ESP32.',
|
||||
description: 'Count 0–9 on a 7-segment display driven from GPIO 12, 13, 14, 22, 25, 26, 27 on the ESP32.',
|
||||
category: 'displays',
|
||||
difficulty: 'beginner',
|
||||
boardType: 'esp32',
|
||||
boardFilter: 'esp32',
|
||||
code: `// ESP32 — 7-Segment Display Counter 0-9
|
||||
// Segments: a=12, b=13, c=14, d=15, e=16, f=17, g=18
|
||||
// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=22
|
||||
|
||||
const int SEG[7] = {12, 13, 14, 15, 16, 17, 18};
|
||||
const int SEG[7] = {12, 13, 14, 25, 26, 27, 22};
|
||||
|
||||
const bool DIGITS[10][7] = {
|
||||
{1,1,1,1,1,1,0}, // 0
|
||||
|
|
@ -2873,13 +2936,14 @@ void loop() {
|
|||
{ type: 'wokwi-7segment', id: 'esp-seg1', x: 440, y: 140, properties: { common: 'cathode', color: 'orange' } },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'es-a', start: { componentId: 'arduino-uno', pinName: 'GPIO12' }, end: { componentId: 'esp-seg1', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'es-b', start: { componentId: 'arduino-uno', pinName: 'GPIO13' }, end: { componentId: 'esp-seg1', pinName: 'B' }, color: '#ff8800' },
|
||||
{ id: 'es-c', start: { componentId: 'arduino-uno', pinName: 'GPIO14' }, end: { componentId: 'esp-seg1', pinName: 'C' }, color: '#ffcc00' },
|
||||
{ id: 'es-d', start: { componentId: 'arduino-uno', pinName: 'GPIO15' }, end: { componentId: 'esp-seg1', pinName: 'D' }, color: '#44cc44' },
|
||||
{ id: 'es-e', start: { componentId: 'arduino-uno', pinName: 'GPIO16' }, end: { componentId: 'esp-seg1', pinName: 'E' }, color: '#4488ff' },
|
||||
{ id: 'es-f', start: { componentId: 'arduino-uno', pinName: 'GPIO17' }, end: { componentId: 'esp-seg1', pinName: 'F' }, color: '#aa44ff' },
|
||||
{ id: 'es-g', start: { componentId: 'arduino-uno', pinName: 'GPIO18' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' },
|
||||
{ id: 'es-a', start: { componentId: 'esp32', pinName: '12' }, end: { componentId: 'esp-seg1', pinName: 'A' }, color: '#ff4444' },
|
||||
{ id: 'es-b', start: { componentId: 'esp32', pinName: '13' }, end: { componentId: 'esp-seg1', pinName: 'B' }, color: '#ff8800' },
|
||||
{ id: 'es-c', start: { componentId: 'esp32', pinName: '14' }, end: { componentId: 'esp-seg1', pinName: 'C' }, color: '#ffcc00' },
|
||||
{ id: 'es-d', start: { componentId: 'esp32', pinName: '25' }, end: { componentId: 'esp-seg1', pinName: 'D' }, color: '#44cc44' },
|
||||
{ id: 'es-e', start: { componentId: 'esp32', pinName: '26' }, end: { componentId: 'esp-seg1', pinName: 'E' }, color: '#4488ff' },
|
||||
{ id: 'es-f', start: { componentId: 'esp32', pinName: '27' }, end: { componentId: 'esp-seg1', pinName: 'F' }, color: '#aa44ff' },
|
||||
{ id: 'es-g', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' },
|
||||
{ id: 'es-gnd', start: { componentId: 'esp-seg1', pinName: 'COM' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -2920,6 +2984,8 @@ void loop() {
|
|||
],
|
||||
wires: [
|
||||
{ id: 'w-pot-sig', start: { componentId: 'arduino-uno', pinName: 'A0' }, end: { componentId: 'pot1', pinName: 'SIG' }, color: '#aa44ff' },
|
||||
{ id: 'w-pot-vcc', start: { componentId: 'arduino-uno', pinName: '5V' }, end: { componentId: 'pot1', pinName: 'VCC' }, color: '#ff0000' },
|
||||
{ id: 'w-pot-gnd', start: { componentId: 'arduino-uno', pinName: 'GND' }, end: { componentId: 'pot1', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2963,6 +3029,7 @@ void loop() {
|
|||
{ id: 'w-r', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'rgb1', pinName: 'R' }, color: '#ff2222' },
|
||||
{ id: 'w-g', start: { componentId: 'arduino-uno', pinName: '10' }, end: { componentId: 'rgb1', pinName: 'G' }, color: '#22cc22' },
|
||||
{ id: 'w-b', start: { componentId: 'arduino-uno', pinName: '11' }, end: { componentId: 'rgb1', pinName: 'B' }, color: '#2244ff' },
|
||||
{ id: 'w-rgb-gnd', start: { componentId: 'rgb1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -2997,8 +3064,10 @@ void loop() {
|
|||
{ type: 'wokwi-led', id: 'pico-led-btn', x: 440, y: 260, properties: { color: 'yellow' } },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'pb-btn', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'pico-btn1', pinName: '1a' }, color: '#00aaff' },
|
||||
{ id: 'pb-led', start: { componentId: 'nano-rp2040', pinName: 'D3' }, end: { componentId: 'pico-led-btn', pinName: 'A' }, color: '#ffcc00' },
|
||||
{ id: 'pb-btn', start: { componentId: 'nano-rp2040', pinName: 'GP2' }, end: { componentId: 'pico-btn1', pinName: '1a' }, color: '#00aaff' },
|
||||
{ id: 'pb-led', start: { componentId: 'nano-rp2040', pinName: 'GP3' }, end: { componentId: 'pico-led-btn', pinName: 'A' }, color: '#ffcc00' },
|
||||
{ id: 'pb-led-gnd', start: { componentId: 'pico-led-btn', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
{ id: 'pb-btn-gnd', start: { componentId: 'pico-btn1', pinName: '1b' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -3042,9 +3111,10 @@ void loop() {
|
|||
{ type: 'wokwi-rgb-led', id: 'pico-rgb1', x: 440, y: 160, properties: {} },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'pr-r', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'pico-rgb1', pinName: 'R' }, color: '#ff2222' },
|
||||
{ id: 'pr-g', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'pico-rgb1', pinName: 'G' }, color: '#22cc22' },
|
||||
{ id: 'pr-b', start: { componentId: 'nano-rp2040', pinName: 'D8' }, end: { componentId: 'pico-rgb1', pinName: 'B' }, color: '#2244ff' },
|
||||
{ id: 'pr-r', start: { componentId: 'nano-rp2040', pinName: 'GP6' }, end: { componentId: 'pico-rgb1', pinName: 'R' }, color: '#ff2222' },
|
||||
{ id: 'pr-g', start: { componentId: 'nano-rp2040', pinName: 'GP7' }, end: { componentId: 'pico-rgb1', pinName: 'G' }, color: '#22cc22' },
|
||||
{ id: 'pr-b', start: { componentId: 'nano-rp2040', pinName: 'GP8' }, end: { componentId: 'pico-rgb1', pinName: 'B' }, color: '#2244ff' },
|
||||
{ id: 'pr-gnd', start: { componentId: 'pico-rgb1', pinName: 'COM' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -207,4 +207,9 @@ PartSimulationRegistry.register('7segment', {
|
|||
|
||||
return () => unsubscribers.forEach(u => u());
|
||||
},
|
||||
// Called by SimulatorCanvas for boards without a local simulator (e.g. ESP32 via QEMU backend).
|
||||
// pinName is the segment identifier (A, B, C, D, E, F, G, DP).
|
||||
onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => {
|
||||
set7SegPin(element, pinName, state);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
* Oscilloscope / Logic Analyzer store.
|
||||
*
|
||||
* Captures pin HIGH/LOW transitions with microsecond-level timestamps
|
||||
* derived from the AVR CPU cycle counter and renders them as waveforms.
|
||||
* derived from the CPU cycle counter and renders them as waveforms.
|
||||
*
|
||||
* Channels are keyed by (boardId, pin) so multiple boards with the same
|
||||
* logical pin number can be monitored independently.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
|
@ -23,6 +26,8 @@ export const CHANNEL_COLORS = [
|
|||
|
||||
export interface OscChannel {
|
||||
id: string;
|
||||
/** Board that owns this channel */
|
||||
boardId: string;
|
||||
pin: number;
|
||||
label: string;
|
||||
color: string;
|
||||
|
|
@ -51,7 +56,7 @@ interface OscilloscopeState {
|
|||
toggleOscilloscope: () => void;
|
||||
setCapturing: (running: boolean) => void;
|
||||
setTimeDivMs: (ms: number) => void;
|
||||
addChannel: (pin: number) => void;
|
||||
addChannel: (boardId: string, pin: number, pinLabel: string) => void;
|
||||
removeChannel: (id: string) => void;
|
||||
/** Push one sample; drops the oldest if the buffer is full */
|
||||
pushSample: (channelId: string, timeMs: number, state: boolean) => void;
|
||||
|
|
@ -71,18 +76,16 @@ export const useOscilloscopeStore = create<OscilloscopeState>((set, get) => ({
|
|||
|
||||
setTimeDivMs: (ms) => set({ timeDivMs: ms }),
|
||||
|
||||
addChannel: (pin: number) => {
|
||||
addChannel: (boardId: string, pin: number, pinLabel: string) => {
|
||||
const { channels } = get();
|
||||
if (channels.some((c) => c.pin === pin)) return; // already added
|
||||
// Deduplicate by (boardId, pin)
|
||||
if (channels.some((c) => c.boardId === boardId && c.pin === pin)) return;
|
||||
|
||||
const isAnalog = pin >= 14 && pin <= 19;
|
||||
const pinLabel = isAnalog ? `A${pin - 14}` : `D${pin}`;
|
||||
|
||||
const id = `osc-ch-${pin}`;
|
||||
const id = `osc-ch-${boardId}-${pin}`;
|
||||
const color = CHANNEL_COLORS[channels.length % CHANNEL_COLORS.length];
|
||||
|
||||
set((s) => ({
|
||||
channels: [...s.channels, { id, pin, label: pinLabel, color }],
|
||||
channels: [...s.channels, { id, boardId, pin, label: pinLabel, color }],
|
||||
samples: { ...s.samples, [id]: [] },
|
||||
}));
|
||||
},
|
||||
|
|
@ -103,9 +106,8 @@ export const useOscilloscopeStore = create<OscilloscopeState>((set, get) => ({
|
|||
const buf = s.samples[channelId];
|
||||
if (!buf) return s;
|
||||
|
||||
// Efficient copy: one allocation instead of two (avoids spread + slice).
|
||||
const next = buf.slice();
|
||||
if (next.length >= MAX_SAMPLES) next.shift(); // drop oldest
|
||||
if (next.length >= MAX_SAMPLES) next.shift();
|
||||
next.push({ timeMs, state });
|
||||
return { samples: { ...s.samples, [channelId]: next } };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -213,11 +213,11 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const initialPm = new PinManager();
|
||||
pinManagerMap.set(INITIAL_BOARD_ID, initialPm);
|
||||
|
||||
function getOscilloscopeCallback() {
|
||||
function getOscilloscopeCallback(boardId: string) {
|
||||
return (pin: number, state: boolean, timeMs: number) => {
|
||||
const { channels, pushSample } = useOscilloscopeStore.getState();
|
||||
for (const ch of channels) {
|
||||
if (ch.pin === pin) pushSample(ch.id, timeMs, state);
|
||||
if (ch.boardId === boardId && ch.pin === pin) pushSample(ch.id, timeMs, state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
return { boards, ...(isActive ? { serialBaudRate: baud } : {}) };
|
||||
});
|
||||
},
|
||||
getOscilloscopeCallback(),
|
||||
getOscilloscopeCallback(INITIAL_BOARD_ID),
|
||||
);
|
||||
// Cross-board serial bridge for the initial board: AVR TX → Pi bridges RX
|
||||
const initialOrigSerial = initialSim.onSerialData;
|
||||
|
|
@ -341,7 +341,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
return { boards, ...(isActive ? { serialBaudRate: baud } : {}) };
|
||||
});
|
||||
},
|
||||
getOscilloscopeCallback(),
|
||||
getOscilloscopeCallback(id),
|
||||
);
|
||||
// Cross-board serial bridge: AVR TX → all Pi bridges RX
|
||||
const origSerial = sim.onSerialData;
|
||||
|
|
@ -948,8 +948,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const component = state.components.find((c) => c.id === componentId);
|
||||
// Check if this componentId matches a board id
|
||||
const board = state.boards.find((b) => b.id === componentId);
|
||||
const compX = component ? component.x : (board ? board.x : state.boardPosition.x);
|
||||
const compY = component ? component.y : (board ? board.y : state.boardPosition.y);
|
||||
// Components have a DynamicComponent wrapper with border:2px + padding:4px → offset (4,6)
|
||||
// Boards are rendered directly without a wrapper, so no offset.
|
||||
const compX = component ? component.x + 4 : (board ? board.x : state.boardPosition.x);
|
||||
const compY = component ? component.y + 6 : (board ? board.y : state.boardPosition.y);
|
||||
|
||||
const updatedWires = state.wires.map((wire) => {
|
||||
const updated = { ...wire };
|
||||
|
|
@ -972,21 +974,21 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const updatedWires = state.wires.map((wire) => {
|
||||
const updated = { ...wire };
|
||||
|
||||
// Resolve start
|
||||
// Resolve start — components have wrapper offset (4,6), boards do not
|
||||
const startComp = state.components.find((c) => c.id === wire.start.componentId);
|
||||
const startBoard = state.boards.find((b) => b.id === wire.start.componentId);
|
||||
const startX = startComp ? startComp.x : (startBoard ? startBoard.x : state.boardPosition.x);
|
||||
const startY = startComp ? startComp.y : (startBoard ? startBoard.y : state.boardPosition.y);
|
||||
const startX = startComp ? startComp.x + 4 : (startBoard ? startBoard.x : state.boardPosition.x);
|
||||
const startY = startComp ? startComp.y + 6 : (startBoard ? startBoard.y : state.boardPosition.y);
|
||||
const startPos = calculatePinPosition(wire.start.componentId, wire.start.pinName, startX, startY);
|
||||
updated.start = startPos
|
||||
? { ...wire.start, x: startPos.x, y: startPos.y }
|
||||
: { ...wire.start, x: startX, y: startY };
|
||||
|
||||
// Resolve end
|
||||
// Resolve end — components have wrapper offset (4,6), boards do not
|
||||
const endComp = state.components.find((c) => c.id === wire.end.componentId);
|
||||
const endBoard = state.boards.find((b) => b.id === wire.end.componentId);
|
||||
const endX = endComp ? endComp.x : (endBoard ? endBoard.x : state.boardPosition.x);
|
||||
const endY = endComp ? endComp.y : (endBoard ? endBoard.y : state.boardPosition.y);
|
||||
const endX = endComp ? endComp.x + 4 : (endBoard ? endBoard.x : state.boardPosition.x);
|
||||
const endY = endComp ? endComp.y + 6 : (endBoard ? endBoard.y : state.boardPosition.y);
|
||||
const endPos = calculatePinPosition(wire.end.componentId, wire.end.pinName, endX, endY);
|
||||
updated.end = endPos
|
||||
? { ...wire.end, x: endPos.x, y: endPos.y }
|
||||
|
|
|
|||
Loading…
Reference in New Issue