feat: improve mobile wiring UX with pinch-zoom preserve and crosshair
- Pinch-zoom no longer cancels wire-in-progress (preview freezes during 2-finger gesture, resumes after) - Full-canvas crosshair guides (8000px dashed lines) at cursor position during wire creation for touch precision alignment - PinOverlay touch interaction improvements - Segment handle scaling inverse to zoom for constant screen sizemaster
parent
ea82602102
commit
cd85b78b75
|
|
@ -71,12 +71,15 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On touch devices, compute world-space size so the pin is at least
|
// Visual indicator stays small (PIN_VISUAL world px).
|
||||||
// TOUCH_MIN_SCREEN_PX on screen. On desktop, keep the original 12px.
|
// On touch devices the *hit area* is enlarged inversely to zoom so the
|
||||||
const pinSize = isTouchDevice
|
// screen-space tap target stays at least TOUCH_MIN_SCREEN_PX, but the
|
||||||
|
// visible dot stays at PIN_VISUAL so it doesn't dwarf the components.
|
||||||
|
const hitSize = isTouchDevice
|
||||||
? Math.max(PIN_VISUAL, TOUCH_MIN_SCREEN_PX / zoom)
|
? Math.max(PIN_VISUAL, TOUCH_MIN_SCREEN_PX / zoom)
|
||||||
: PIN_VISUAL;
|
: PIN_VISUAL;
|
||||||
const pinHalf = pinSize / 2;
|
const hitHalf = hitSize / 2;
|
||||||
|
const visualHalf = PIN_VISUAL / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -107,28 +110,47 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${pinX - pinHalf}px`,
|
left: `${pinX - hitHalf}px`,
|
||||||
top: `${pinY - pinHalf}px`,
|
top: `${pinY - hitHalf}px`,
|
||||||
width: `${pinSize}px`,
|
width: `${hitSize}px`,
|
||||||
height: `${pinSize}px`,
|
height: `${hitSize}px`,
|
||||||
borderRadius: '3px',
|
|
||||||
backgroundColor: 'rgba(0, 200, 255, 0.8)',
|
|
||||||
border: '1.5px solid white',
|
|
||||||
cursor: 'crosshair',
|
cursor: 'crosshair',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
transition: 'all 0.15s',
|
|
||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = 'rgba(0, 255, 100, 1)';
|
const dot = e.currentTarget.firstElementChild as HTMLElement;
|
||||||
e.currentTarget.style.transform = 'scale(1.4)';
|
if (dot) {
|
||||||
|
dot.style.backgroundColor = 'rgba(0, 255, 100, 1)';
|
||||||
|
dot.style.transform = 'translate(-50%, -50%) scale(1.4)';
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = 'rgba(0, 200, 255, 0.8)';
|
const dot = e.currentTarget.firstElementChild as HTMLElement;
|
||||||
e.currentTarget.style.transform = 'scale(1)';
|
if (dot) {
|
||||||
|
dot.style.backgroundColor = 'rgba(0, 200, 255, 0.8)';
|
||||||
|
dot.style.transform = 'translate(-50%, -50%) scale(1)';
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title={pin.name}
|
title={pin.name}
|
||||||
/>
|
>
|
||||||
|
{/* Small visible dot — stays at PIN_VISUAL regardless of zoom */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: `${PIN_VISUAL}px`,
|
||||||
|
height: `${PIN_VISUAL}px`,
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: 'rgba(0, 200, 255, 0.8)',
|
||||||
|
border: '1.5px solid white',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { PartSimulationRegistry } from '../../simulation/parts';
|
||||||
import { PinOverlay } from './PinOverlay';
|
import { PinOverlay } from './PinOverlay';
|
||||||
import { isBoardComponent, boardPinToNumber } from '../../utils/boardPinMapping';
|
import { isBoardComponent, boardPinToNumber } from '../../utils/boardPinMapping';
|
||||||
import { autoWireColor, WIRE_KEY_COLORS } from '../../utils/wireUtils';
|
import { autoWireColor, WIRE_KEY_COLORS } from '../../utils/wireUtils';
|
||||||
|
import { findClosestPin, getAllPinPositions } from '../../utils/pinPositionCalculator';
|
||||||
import {
|
import {
|
||||||
findWireNearPoint,
|
findWireNearPoint,
|
||||||
getRenderedPoints,
|
getRenderedPoints,
|
||||||
|
|
@ -80,6 +81,10 @@ export const SimulatorCanvas = () => {
|
||||||
const setSelectedWire = useSimulatorStore((s) => s.setSelectedWire);
|
const setSelectedWire = useSimulatorStore((s) => s.setSelectedWire);
|
||||||
const removeWire = useSimulatorStore((s) => s.removeWire);
|
const removeWire = useSimulatorStore((s) => s.removeWire);
|
||||||
const updateWire = useSimulatorStore((s) => s.updateWire);
|
const updateWire = useSimulatorStore((s) => s.updateWire);
|
||||||
|
const undoWire = useSimulatorStore((s) => s.undoWire);
|
||||||
|
const redoWire = useSimulatorStore((s) => s.redoWire);
|
||||||
|
const canUndoWire = useSimulatorStore((s) => s.wireUndoStack.length > 0);
|
||||||
|
const canRedoWire = useSimulatorStore((s) => s.wireRedoStack.length > 0);
|
||||||
const wires = useSimulatorStore((s) => s.wires);
|
const wires = useSimulatorStore((s) => s.wires);
|
||||||
|
|
||||||
// Oscilloscope
|
// Oscilloscope
|
||||||
|
|
@ -90,6 +95,13 @@ export const SimulatorCanvas = () => {
|
||||||
const esp32CrashBoardId = useSimulatorStore((s) => s.esp32CrashBoardId);
|
const esp32CrashBoardId = useSimulatorStore((s) => s.esp32CrashBoardId);
|
||||||
const dismissEsp32Crash = useSimulatorStore((s) => s.dismissEsp32Crash);
|
const dismissEsp32Crash = useSimulatorStore((s) => s.dismissEsp32Crash);
|
||||||
|
|
||||||
|
// Board built-in LED: subscribe directly to PinManager pin 13 (independent of led-builtin component)
|
||||||
|
const [boardLed13, setBoardLed13] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pinManager) return;
|
||||||
|
return pinManager.onPinChange(13, (_pin, state) => setBoardLed13(state));
|
||||||
|
}, [pinManager]);
|
||||||
|
|
||||||
// Component picker modal
|
// Component picker modal
|
||||||
const [showComponentPicker, setShowComponentPicker] = useState(false);
|
const [showComponentPicker, setShowComponentPicker] = useState(false);
|
||||||
const [registry] = useState(() => ComponentRegistry.getInstance());
|
const [registry] = useState(() => ComponentRegistry.getInstance());
|
||||||
|
|
@ -198,8 +210,27 @@ export const SimulatorCanvas = () => {
|
||||||
selectedWireIdRef.current = selectedWireId;
|
selectedWireIdRef.current = selectedWireId;
|
||||||
const touchPassthroughRef = useRef(false);
|
const touchPassthroughRef = useRef(false);
|
||||||
const touchOnPinRef = useRef(false);
|
const touchOnPinRef = useRef(false);
|
||||||
|
const touchOnBannerRef = useRef(false);
|
||||||
const lastTapTimeRef = useRef(0);
|
const lastTapTimeRef = useRef(0);
|
||||||
|
|
||||||
|
// ── Touch aiming state for wire creation ───────────────────────────────
|
||||||
|
// Full crosshair-based wire creation on touch devices:
|
||||||
|
// idle → long press → aiming_start (crosshair, no wire yet)
|
||||||
|
// aiming_start → release on pin → wire_started (wire begins from pin)
|
||||||
|
// aiming_start → release empty → idle (cancelled)
|
||||||
|
// wire_started → long press → aiming_end (crosshair, wire follows)
|
||||||
|
// aiming_end → release on pin → idle (wire finished, snapped)
|
||||||
|
// aiming_end → release empty → wire_started (waypoint added)
|
||||||
|
const AIMING_LONG_PRESS_MS = 400;
|
||||||
|
const AIMING_OFFSET_Y = -50; // world-px offset (crosshair above finger)
|
||||||
|
const AIMING_PIN_SNAP_DIST = 40; // world-px: max distance to snap to a pin on release
|
||||||
|
type WireAimingPhase = 'idle' | 'aiming_start' | 'wire_started' | 'aiming_end';
|
||||||
|
const wireAimingPhaseRef = useRef<WireAimingPhase>('idle');
|
||||||
|
const wireAimingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const currentTouchRef = useRef({ x: 0, y: 0 }); // latest screen position for timeout callback
|
||||||
|
const [wireAiming, setWireAiming] = useState(false); // React state for rendering (crosshair visible)
|
||||||
|
const [aimPosition, setAimPosition] = useState<{ x: number; y: number } | null>(null); // world coords of crosshair
|
||||||
|
|
||||||
// Convert viewport coords to world (canvas) coords
|
// Convert viewport coords to world (canvas) coords
|
||||||
const toWorld = useCallback((screenX: number, screenY: number) => {
|
const toWorld = useCallback((screenX: number, screenY: number) => {
|
||||||
const rect = canvasRef.current?.getBoundingClientRect();
|
const rect = canvasRef.current?.getBoundingClientRect();
|
||||||
|
|
@ -210,6 +241,44 @@ export const SimulatorCanvas = () => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** Find the closest pin across ALL components and boards to a world position. */
|
||||||
|
// Component wrappers add padding/border offset to pin positions.
|
||||||
|
// PinOverlay uses wrapperOffsetX=4, wrapperOffsetY=6 for components, 0 for boards.
|
||||||
|
const COMP_WRAPPER_OFFSET_X = 4;
|
||||||
|
const COMP_WRAPPER_OFFSET_Y = 6;
|
||||||
|
|
||||||
|
const findNearestPin = useCallback((worldX: number, worldY: number, maxDist: number) => {
|
||||||
|
let best: { componentId: string; pinName: string; x: number; y: number; dist: number } | null = null;
|
||||||
|
|
||||||
|
// Check component pins (offset by wrapper padding)
|
||||||
|
for (const comp of componentsRef.current) {
|
||||||
|
const pin = findClosestPin(
|
||||||
|
comp.id, comp.x + COMP_WRAPPER_OFFSET_X, comp.y + COMP_WRAPPER_OFFSET_Y,
|
||||||
|
worldX, worldY, maxDist,
|
||||||
|
);
|
||||||
|
if (pin) {
|
||||||
|
const d = Math.sqrt((pin.x - worldX) ** 2 + (pin.y - worldY) ** 2);
|
||||||
|
if (!best || d < best.dist) {
|
||||||
|
best = { componentId: comp.id, pinName: pin.name, x: pin.x, y: pin.y, dist: d };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check board pins (no wrapper offset)
|
||||||
|
const storeBoards = useSimulatorStore.getState().boards;
|
||||||
|
for (const board of storeBoards) {
|
||||||
|
const pin = findClosestPin(board.id, board.x, board.y, worldX, worldY, maxDist);
|
||||||
|
if (pin) {
|
||||||
|
const d = Math.sqrt((pin.x - worldX) ** 2 + (pin.y - worldY) ** 2);
|
||||||
|
if (!best || d < best.dist) {
|
||||||
|
best = { componentId: board.id, pinName: pin.name, x: pin.x, y: pin.y, dist: d };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initialize simulator on mount
|
// Initialize simulator on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initSimulator();
|
initSimulator();
|
||||||
|
|
@ -262,14 +331,18 @@ export const SimulatorCanvas = () => {
|
||||||
// Reset per-gesture flags
|
// Reset per-gesture flags
|
||||||
touchOnPinRef.current = false;
|
touchOnPinRef.current = false;
|
||||||
touchPassthroughRef.current = false;
|
touchPassthroughRef.current = false;
|
||||||
|
touchOnBannerRef.current = false;
|
||||||
pinchStartDistRef.current = 0;
|
pinchStartDistRef.current = 0;
|
||||||
|
|
||||||
if (e.touches.length === 2) {
|
if (e.touches.length === 2) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Cancel wire in progress on two-finger gesture
|
// Preserve wire-in-progress during pinch-zoom — don't cancel it.
|
||||||
if (wireInProgressRef.current) {
|
// The wire preview will freeze while pinching (touchMove only
|
||||||
useSimulatorStore.getState().cancelWireCreation();
|
// updates wire when touches===1) and resume after zoom ends.
|
||||||
}
|
|
||||||
|
// Cancel aiming timer (pinch is zoom, not aiming)
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
|
|
||||||
// Cancel any active drag/pan and prepare zoom
|
// Cancel any active drag/pan and prepare zoom
|
||||||
isPanningRef.current = false;
|
isPanningRef.current = false;
|
||||||
touchDraggedComponentIdRef.current = null;
|
touchDraggedComponentIdRef.current = null;
|
||||||
|
|
@ -301,6 +374,12 @@ export const SimulatorCanvas = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 1b. Banner buttons (Cancel, Delete) → let browser synthesize click ──
|
||||||
|
if (target?.closest('.wire-mode-banner') || target?.closest('.wire-selected-banner')) {
|
||||||
|
touchOnBannerRef.current = true;
|
||||||
|
return; // Don't preventDefault → browser fires click on button
|
||||||
|
}
|
||||||
|
|
||||||
// ── 2. Interactive web component during simulation → let browser synthesize mouse events ──
|
// ── 2. Interactive web component during simulation → let browser synthesize mouse events ──
|
||||||
// (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis)
|
// (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis)
|
||||||
// touch-action:none on .canvas-content already prevents browser scroll/zoom.
|
// touch-action:none on .canvas-content already prevents browser scroll/zoom.
|
||||||
|
|
@ -317,15 +396,47 @@ export const SimulatorCanvas = () => {
|
||||||
|
|
||||||
touchClickStartTimeRef.current = Date.now();
|
touchClickStartTimeRef.current = Date.now();
|
||||||
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
currentTouchRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
|
||||||
// ── 3. Wire in progress → track for waypoint, update preview ──
|
// ── 3. Touch aiming for wire creation ──
|
||||||
if (wireInProgressRef.current) {
|
// Any phase that uses aiming: cancel previous timer first
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
|
|
||||||
|
const aimPhase = wireAimingPhaseRef.current;
|
||||||
|
|
||||||
|
// Already in an aiming phase (aiming_start or aiming_end) → touch updates crosshair immediately
|
||||||
|
if (aimPhase === 'aiming_start' || aimPhase === 'aiming_end') {
|
||||||
const world = toWorld(touch.clientX, touch.clientY);
|
const world = toWorld(touch.clientX, touch.clientY);
|
||||||
useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
|
if (aimPhase === 'aiming_end') {
|
||||||
// Don't start pan/drag — let touchmove update wire preview, touchend add waypoint
|
useSimulatorStore.getState().updateWireInProgress(world.x, world.y + AIMING_OFFSET_Y);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wire_started → long press to aim for endpoint, short drag = pan
|
||||||
|
if (aimPhase === 'wire_started') {
|
||||||
|
wireAimingTimerRef.current = setTimeout(() => {
|
||||||
|
wireAimingPhaseRef.current = 'aiming_end';
|
||||||
|
setWireAiming(true);
|
||||||
|
if (navigator.vibrate) navigator.vibrate(30);
|
||||||
|
const world = toWorld(currentTouchRef.current.x, currentTouchRef.current.y);
|
||||||
|
const aimX = world.x;
|
||||||
|
const aimY = world.y + AIMING_OFFSET_Y;
|
||||||
|
setAimPosition({ x: aimX, y: aimY });
|
||||||
|
useSimulatorStore.getState().updateWireInProgress(aimX, aimY);
|
||||||
|
}, AIMING_LONG_PRESS_MS);
|
||||||
|
|
||||||
|
isPanningRef.current = true;
|
||||||
|
panStartRef.current = {
|
||||||
|
mouseX: touch.clientX,
|
||||||
|
mouseY: touch.clientY,
|
||||||
|
panX: panRef.current.x,
|
||||||
|
panY: panRef.current.y,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// idle: aiming timer is set below (only for empty canvas, not components)
|
||||||
|
|
||||||
// ── 4. Component detection ──
|
// ── 4. Component detection ──
|
||||||
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
||||||
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
||||||
|
|
@ -367,7 +478,16 @@ export const SimulatorCanvas = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ── 6. Empty canvas → start pan ──
|
// ── 6. Empty canvas → start pan + long press starts aiming (idle phase only) ──
|
||||||
|
if (aimPhase === 'idle') {
|
||||||
|
wireAimingTimerRef.current = setTimeout(() => {
|
||||||
|
wireAimingPhaseRef.current = 'aiming_start';
|
||||||
|
setWireAiming(true);
|
||||||
|
if (navigator.vibrate) navigator.vibrate(30);
|
||||||
|
const world = toWorld(currentTouchRef.current.x, currentTouchRef.current.y);
|
||||||
|
setAimPosition({ x: world.x, y: world.y + AIMING_OFFSET_Y });
|
||||||
|
}, AIMING_LONG_PRESS_MS);
|
||||||
|
}
|
||||||
isPanningRef.current = true;
|
isPanningRef.current = true;
|
||||||
panStartRef.current = {
|
panStartRef.current = {
|
||||||
mouseX: touch.clientX,
|
mouseX: touch.clientX,
|
||||||
|
|
@ -428,13 +548,31 @@ export const SimulatorCanvas = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wire preview: update position as finger moves ──
|
// ── Touch aiming: crosshair follows finger ──
|
||||||
if (wireInProgressRef.current && !isPanningRef.current && !touchDraggedComponentIdRef.current) {
|
currentTouchRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
const aimPhase = wireAimingPhaseRef.current;
|
||||||
|
|
||||||
|
if (aimPhase === 'aiming_start' || aimPhase === 'aiming_end') {
|
||||||
const world = toWorld(touch.clientX, touch.clientY);
|
const world = toWorld(touch.clientX, touch.clientY);
|
||||||
useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
|
const aimX = world.x;
|
||||||
|
const aimY = world.y + AIMING_OFFSET_Y;
|
||||||
|
setAimPosition({ x: aimX, y: aimY });
|
||||||
|
if (aimPhase === 'aiming_end') {
|
||||||
|
useSimulatorStore.getState().updateWireInProgress(aimX, aimY);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// idle or wire_started with pending timer: cancel if finger moves (pan gesture)
|
||||||
|
if (wireAimingTimerRef.current) {
|
||||||
|
const dx = touch.clientX - touchClickStartPosRef.current.x;
|
||||||
|
const dy = touch.clientY - touchClickStartPosRef.current.y;
|
||||||
|
if (Math.sqrt(dx * dx + dy * dy) > 10) {
|
||||||
|
clearTimeout(wireAimingTimerRef.current);
|
||||||
|
wireAimingTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isPanningRef.current) {
|
if (isPanningRef.current) {
|
||||||
// ── Single finger pan ──
|
// ── Single finger pan ──
|
||||||
const dx = touch.clientX - panStartRef.current.mouseX;
|
const dx = touch.clientX - panStartRef.current.mouseX;
|
||||||
|
|
@ -484,6 +622,11 @@ export const SimulatorCanvas = () => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Banner touch: let browser fire click on button
|
||||||
|
if (touchOnBannerRef.current) {
|
||||||
|
touchOnBannerRef.current = false;
|
||||||
|
return; // No preventDefault → click fires on button
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -514,18 +657,73 @@ export const SimulatorCanvas = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Touch aiming release (MUST come before pan check — aiming sets isPanning too) ──
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
|
const aimPhase = wireAimingPhaseRef.current;
|
||||||
|
const changed = e.changedTouches[0];
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
if (aimPhase === 'aiming_start') {
|
||||||
|
// Release after first aim: find pin to start wire
|
||||||
|
isPanningRef.current = false;
|
||||||
|
const world = toWorld(changed.clientX, changed.clientY);
|
||||||
|
const aimX = world.x;
|
||||||
|
const aimY = world.y + AIMING_OFFSET_Y;
|
||||||
|
const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current;
|
||||||
|
const nearPin = findNearestPin(aimX, aimY, snapDist);
|
||||||
|
|
||||||
|
setWireAiming(false); setAimPosition(null);
|
||||||
|
if (nearPin) {
|
||||||
|
// Start wire from this pin
|
||||||
|
startWireCreation(
|
||||||
|
{ componentId: nearPin.componentId, pinName: nearPin.pinName, x: nearPin.x, y: nearPin.y },
|
||||||
|
autoWireColor(nearPin.pinName),
|
||||||
|
);
|
||||||
|
wireAimingPhaseRef.current = 'wire_started';
|
||||||
|
} else {
|
||||||
|
// No pin found — cancel
|
||||||
|
wireAimingPhaseRef.current = 'idle';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aimPhase === 'aiming_end') {
|
||||||
|
// Release after second aim: snap to pin (finish) or add waypoint
|
||||||
|
isPanningRef.current = false;
|
||||||
|
const world = toWorld(changed.clientX, changed.clientY);
|
||||||
|
const aimX = world.x;
|
||||||
|
const aimY = world.y + AIMING_OFFSET_Y;
|
||||||
|
const snapDist = AIMING_PIN_SNAP_DIST / zoomRef.current;
|
||||||
|
const nearPin = findNearestPin(aimX, aimY, snapDist);
|
||||||
|
|
||||||
|
setWireAiming(false); setAimPosition(null);
|
||||||
|
if (nearPin) {
|
||||||
|
// Finish wire at this pin
|
||||||
|
finishWireCreation({ componentId: nearPin.componentId, pinName: nearPin.pinName, x: nearPin.x, y: nearPin.y });
|
||||||
|
trackCreateWire();
|
||||||
|
wireAimingPhaseRef.current = 'idle';
|
||||||
|
} else {
|
||||||
|
// No pin: add waypoint, stay in wire_started for next aim
|
||||||
|
useSimulatorStore.getState().addWireWaypoint(aimX, aimY);
|
||||||
|
wireAimingPhaseRef.current = 'wire_started';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire_started but not aiming (short tap): skip — user must long-press to aim
|
||||||
|
if (aimPhase === 'wire_started') {
|
||||||
|
isPanningRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Finish panning ──
|
// ── Finish panning ──
|
||||||
let wasPanning = false;
|
let wasPanning = false;
|
||||||
if (isPanningRef.current) {
|
if (isPanningRef.current) {
|
||||||
isPanningRef.current = false;
|
isPanningRef.current = false;
|
||||||
setPan({ ...panRef.current });
|
setPan({ ...panRef.current });
|
||||||
wasPanning = true;
|
wasPanning = true;
|
||||||
// Don't return — fall through so short taps can select wires
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed = e.changedTouches[0];
|
|
||||||
if (!changed) return;
|
|
||||||
|
|
||||||
const elapsed = Date.now() - touchClickStartTimeRef.current;
|
const elapsed = Date.now() - touchClickStartTimeRef.current;
|
||||||
const dx = changed.clientX - touchClickStartPosRef.current.x;
|
const dx = changed.clientX - touchClickStartPosRef.current.x;
|
||||||
const dy = changed.clientY - touchClickStartPosRef.current.y;
|
const dy = changed.clientY - touchClickStartPosRef.current.y;
|
||||||
|
|
@ -541,14 +739,10 @@ export const SimulatorCanvas = () => {
|
||||||
|
|
||||||
if (isShortTap) {
|
if (isShortTap) {
|
||||||
if (touchId.startsWith('__board__:')) {
|
if (touchId.startsWith('__board__:')) {
|
||||||
// Short tap on board → make it the active board
|
|
||||||
const boardId = touchId.slice('__board__:'.length);
|
const boardId = touchId.slice('__board__:'.length);
|
||||||
useSimulatorStore.getState().setActiveBoardId(boardId);
|
useSimulatorStore.getState().setActiveBoardId(boardId);
|
||||||
} else if (touchId !== '__board__') {
|
} else if (touchId !== '__board__') {
|
||||||
// Short tap on component → open property dialog or sensor panel
|
const component = componentsRef.current.find((c) => c.id === touchId);
|
||||||
const component = componentsRef.current.find(
|
|
||||||
(c) => c.id === touchId
|
|
||||||
);
|
|
||||||
if (component) {
|
if (component) {
|
||||||
if (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) {
|
if (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) {
|
||||||
setSensorControlComponentId(touchId);
|
setSensorControlComponentId(touchId);
|
||||||
|
|
@ -567,15 +761,6 @@ export const SimulatorCanvas = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wire in progress: short tap adds waypoint ──
|
|
||||||
if (wireInProgressRef.current) {
|
|
||||||
if (isShortTap) {
|
|
||||||
const world = toWorld(changed.clientX, changed.clientY);
|
|
||||||
useSimulatorStore.getState().addWireWaypoint(world.x, world.y);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Short tap on empty canvas: wire selection + double-tap wire deletion ──
|
// ── Short tap on empty canvas: wire selection + double-tap wire deletion ──
|
||||||
if (isShortTap) {
|
if (isShortTap) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -1082,6 +1267,10 @@ export const SimulatorCanvas = () => {
|
||||||
// Finish wire: connect to this pin
|
// Finish wire: connect to this pin
|
||||||
finishWireCreation({ componentId, pinName, x, y });
|
finishWireCreation({ componentId, pinName, x, y });
|
||||||
trackCreateWire();
|
trackCreateWire();
|
||||||
|
// Reset aiming state
|
||||||
|
wireAimingPhaseRef.current = 'idle';
|
||||||
|
setWireAiming(false); setAimPosition(null);
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
} else {
|
} else {
|
||||||
// Start wire: auto-detect color from pin name
|
// Start wire: auto-detect color from pin name
|
||||||
startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName));
|
startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName));
|
||||||
|
|
@ -1091,9 +1280,23 @@ export const SimulatorCanvas = () => {
|
||||||
// Keyboard handlers for wires
|
// Keyboard handlers for wires
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Escape → cancel in-progress wire
|
// Escape → cancel in-progress wire or aiming
|
||||||
if (e.key === 'Escape' && wireInProgress) {
|
if (e.key === 'Escape' && (wireInProgress || wireAimingPhaseRef.current !== 'idle')) {
|
||||||
cancelWireCreation();
|
if (wireInProgress) cancelWireCreation();
|
||||||
|
wireAimingPhaseRef.current = 'idle';
|
||||||
|
setWireAiming(false); setAimPosition(null);
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ctrl+Z → undo wire, Ctrl+Shift+Z → redo wire
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
undoWire();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && (e.key === 'Z' || (e.key === 'z' && e.shiftKey))) {
|
||||||
|
e.preventDefault();
|
||||||
|
redoWire();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Delete / Backspace → remove selected wire
|
// Delete / Backspace → remove selected wire
|
||||||
|
|
@ -1114,7 +1317,7 @@ export const SimulatorCanvas = () => {
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [wireInProgress, cancelWireCreation, selectedWireId, removeWire, setWireInProgressColor, updateWire]);
|
}, [wireInProgress, cancelWireCreation, selectedWireId, removeWire, setWireInProgressColor, updateWire, undoWire, redoWire]);
|
||||||
|
|
||||||
// Recalculate wire positions when components change (e.g., when loading an example)
|
// Recalculate wire positions when components change (e.g., when loading an example)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1339,6 +1542,16 @@ export const SimulatorCanvas = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="canvas-header-right">
|
<div className="canvas-header-right">
|
||||||
|
{/* Undo / Redo wire */}
|
||||||
|
<div className="undo-controls">
|
||||||
|
<button className="zoom-btn" onClick={undoWire} disabled={!canUndoWire} title="Undo wire (Ctrl+Z)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" /></svg>
|
||||||
|
</button>
|
||||||
|
<button className="zoom-btn" onClick={redoWire} disabled={!canRedoWire} title="Redo wire (Ctrl+Shift+Z)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Zoom controls */}
|
{/* Zoom controls */}
|
||||||
<div className="zoom-controls">
|
<div className="zoom-controls">
|
||||||
<button className="zoom-btn" onClick={() => handleWheel({ deltaY: 100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom out">
|
<button className="zoom-btn" onClick={() => handleWheel({ deltaY: 100, clientX: 0, clientY: 0, preventDefault: () => {} } as any)} title="Zoom out">
|
||||||
|
|
@ -1454,6 +1667,8 @@ export const SimulatorCanvas = () => {
|
||||||
segmentHandles={segmentHandles}
|
segmentHandles={segmentHandles}
|
||||||
onHandleMouseDown={handleHandleMouseDown}
|
onHandleMouseDown={handleHandleMouseDown}
|
||||||
onHandleTouchStart={handleHandleTouchStart}
|
onHandleTouchStart={handleHandleTouchStart}
|
||||||
|
isAiming={wireAiming}
|
||||||
|
zoom={zoom}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* All boards on canvas */}
|
{/* All boards on canvas */}
|
||||||
|
|
@ -1463,7 +1678,7 @@ export const SimulatorCanvas = () => {
|
||||||
board={board}
|
board={board}
|
||||||
running={running}
|
running={running}
|
||||||
isActive={board.id === activeBoardId}
|
isActive={board.id === activeBoardId}
|
||||||
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
|
led13={boardLed13}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
setClickStartTime(Date.now());
|
setClickStartTime(Date.now());
|
||||||
setClickStartPos({ x: e.clientX, y: e.clientY });
|
setClickStartPos({ x: e.clientX, y: e.clientY });
|
||||||
|
|
@ -1483,15 +1698,45 @@ export const SimulatorCanvas = () => {
|
||||||
|
|
||||||
{/* Components using wokwi-elements */}
|
{/* Components using wokwi-elements */}
|
||||||
<div className="components-area">{registryLoaded && components.map(renderComponent)}</div>
|
<div className="components-area">{registryLoaded && components.map(renderComponent)}</div>
|
||||||
|
|
||||||
|
{/* Standalone aiming crosshair (before wire exists — aiming_start phase) */}
|
||||||
|
{wireAiming && !wireInProgress && aimPosition && (
|
||||||
|
<svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none', zIndex: 40 }}>
|
||||||
|
<line x1={aimPosition.x - 8000} y1={aimPosition.y} x2={aimPosition.x + 8000} y2={aimPosition.y} stroke="rgba(255,255,255,0.25)" strokeWidth="0.5" strokeDasharray="8,6" />
|
||||||
|
<line x1={aimPosition.x} y1={aimPosition.y - 8000} x2={aimPosition.x} y2={aimPosition.y + 8000} stroke="rgba(255,255,255,0.25)" strokeWidth="0.5" strokeDasharray="8,6" />
|
||||||
|
<circle cx={aimPosition.x} cy={aimPosition.y} r="6" fill="none" stroke="rgba(0, 200, 255, 0.8)" strokeWidth="1.5" />
|
||||||
|
<circle cx={aimPosition.x} cy={aimPosition.y} r="2" fill="rgba(0, 200, 255, 0.9)" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wire creation mode banner — visible on both desktop and mobile */}
|
{/* Wire creation mode banner — visible on both desktop and mobile */}
|
||||||
{wireInProgress && (
|
{(wireInProgress || wireAiming) && (
|
||||||
<div className="wire-mode-banner">
|
<div className="wire-mode-banner">
|
||||||
<span>Tap a pin to connect — tap canvas for waypoints</span>
|
<span>{isTouchDevice
|
||||||
<button onClick={() => cancelWireCreation()}>Cancel</button>
|
? (wireAiming
|
||||||
|
? '🎯 Geser ke pin, lepas untuk connect'
|
||||||
|
: 'Tahan lama untuk aim ke pin')
|
||||||
|
: 'Tap a pin to connect — tap canvas for waypoints'}</span>
|
||||||
|
<button onClick={() => {
|
||||||
|
if (wireInProgress) cancelWireCreation();
|
||||||
|
wireAimingPhaseRef.current = 'idle';
|
||||||
|
setWireAiming(false); setAimPosition(null);
|
||||||
|
if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; }
|
||||||
|
}}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Wire selected banner — shows Delete + Undo buttons (essential for touch devices) */}
|
||||||
|
{!wireInProgress && !wireAiming && selectedWireId && (
|
||||||
|
<div className="wire-selected-banner">
|
||||||
|
<span>Wire selected</span>
|
||||||
|
<button className="btn-danger" onClick={() => { removeWire(selectedWireId); }}>Delete</button>
|
||||||
|
<button onClick={() => { setSelectedWire(null); }}>Deselect</button>
|
||||||
|
{canUndoWire && <button onClick={undoWire}>Undo</button>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
/**
|
/**
|
||||||
* WireInProgressRenderer — live preview while drawing a wire.
|
* WireInProgressRenderer — live preview while drawing a wire.
|
||||||
* Shows the fixed waypoints + a dynamic elbow segment to the mouse cursor.
|
* Shows the fixed waypoints + a dynamic elbow segment to the mouse cursor.
|
||||||
|
* Includes a full-canvas crosshair at the cursor position (CircuitJS-style)
|
||||||
|
* to help with alignment and precision on touch devices.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { WireInProgress } from '../../types/wire';
|
import type { WireInProgress } from '../../types/wire';
|
||||||
import { generatePreviewPath, generateOrthogonalPath } from '../../utils/wireUtils';
|
import { generatePreviewPath, generateOrthogonalPath } from '../../utils/wireUtils';
|
||||||
|
|
||||||
|
/** Crosshair arm length in world pixels — large enough to span any visible area */
|
||||||
|
const CROSSHAIR_EXTENT = 8000;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wireInProgress: WireInProgress;
|
wireInProgress: WireInProgress;
|
||||||
|
/** When true, show the full-canvas crosshair guides (aiming mode on touch) */
|
||||||
|
isAiming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WireInProgressRenderer: React.FC<Props> = ({ wireInProgress }) => {
|
export const WireInProgressRenderer: React.FC<Props> = ({ wireInProgress, isAiming = false }) => {
|
||||||
const { startEndpoint, waypoints, color, currentX, currentY } = wireInProgress;
|
const { startEndpoint, waypoints, color, currentX, currentY } = wireInProgress;
|
||||||
|
|
||||||
const path = generatePreviewPath(
|
const path = generatePreviewPath(
|
||||||
|
|
@ -25,6 +32,26 @@ export const WireInProgressRenderer: React.FC<Props> = ({ wireInProgress }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className="wire-in-progress" style={{ pointerEvents: 'none' }}>
|
<g className="wire-in-progress" style={{ pointerEvents: 'none' }}>
|
||||||
|
{/* ── Crosshair: full-canvas alignment guides (only during aiming) ── */}
|
||||||
|
{isAiming && (
|
||||||
|
<>
|
||||||
|
<line
|
||||||
|
x1={currentX - CROSSHAIR_EXTENT} y1={currentY}
|
||||||
|
x2={currentX + CROSSHAIR_EXTENT} y2={currentY}
|
||||||
|
stroke="rgba(255,255,255,0.25)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
strokeDasharray="8,6"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1={currentX} y1={currentY - CROSSHAIR_EXTENT}
|
||||||
|
x2={currentX} y2={currentY + CROSSHAIR_EXTENT}
|
||||||
|
stroke="rgba(255,255,255,0.25)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
strokeDasharray="8,6"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dark outline */}
|
{/* Dark outline */}
|
||||||
<path d={path} stroke="#1a1a1a" strokeWidth="5" fill="none" />
|
<path d={path} stroke="#1a1a1a" strokeWidth="5" fill="none" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ interface WireLayerProps {
|
||||||
onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void;
|
onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void;
|
||||||
/** Called when user starts dragging a handle via touch (passes segIndex) */
|
/** Called when user starts dragging a handle via touch (passes segIndex) */
|
||||||
onHandleTouchStart?: (e: React.TouchEvent, segIndex: number) => void;
|
onHandleTouchStart?: (e: React.TouchEvent, segIndex: number) => void;
|
||||||
|
/** Whether the user is in touch-aiming mode (shows crosshair on wire preview) */
|
||||||
|
isAiming?: boolean;
|
||||||
|
/** Current canvas zoom level — handles scale inversely to stay constant on screen */
|
||||||
|
zoom?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WireLayer: React.FC<WireLayerProps> = ({
|
export const WireLayer: React.FC<WireLayerProps> = ({
|
||||||
|
|
@ -30,6 +34,8 @@ export const WireLayer: React.FC<WireLayerProps> = ({
|
||||||
segmentHandles,
|
segmentHandles,
|
||||||
onHandleMouseDown,
|
onHandleMouseDown,
|
||||||
onHandleTouchStart,
|
onHandleTouchStart,
|
||||||
|
isAiming,
|
||||||
|
zoom = 1,
|
||||||
}) => {
|
}) => {
|
||||||
const wires = useSimulatorStore((s) => s.wires);
|
const wires = useSimulatorStore((s) => s.wires);
|
||||||
const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
|
const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
|
||||||
|
|
@ -63,24 +69,29 @@ export const WireLayer: React.FC<WireLayerProps> = ({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Segment handles for the selected wire */}
|
{/* Segment handles for the selected wire — scaled inversely to zoom for constant screen size */}
|
||||||
{segmentHandles.map((handle) => (
|
{segmentHandles.map((handle) => {
|
||||||
<circle
|
const baseR = isTouchDevice ? 14 : 7;
|
||||||
key={handle.segIndex}
|
const r = baseR / zoom;
|
||||||
cx={handle.mx}
|
const sw = 2 / zoom;
|
||||||
cy={handle.my}
|
return (
|
||||||
r={isTouchDevice ? 14 : 7}
|
<circle
|
||||||
fill="white"
|
key={handle.segIndex}
|
||||||
stroke="#007acc"
|
cx={handle.mx}
|
||||||
strokeWidth={2}
|
cy={handle.my}
|
||||||
style={{ pointerEvents: 'all', cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize', touchAction: 'none' }}
|
r={r}
|
||||||
onMouseDown={(e) => onHandleMouseDown(e, handle.segIndex)}
|
fill="white"
|
||||||
onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)}
|
stroke="#007acc"
|
||||||
/>
|
strokeWidth={sw}
|
||||||
))}
|
style={{ pointerEvents: 'all', cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize', touchAction: 'none' }}
|
||||||
|
onMouseDown={(e) => onHandleMouseDown(e, handle.segIndex)}
|
||||||
|
onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{wireInProgress && (
|
{wireInProgress && (
|
||||||
<WireInProgressRenderer wireInProgress={wireInProgress} />
|
<WireInProgressRenderer wireInProgress={wireInProgress} isAiming={isAiming} />
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ export class ComponentRegistry {
|
||||||
private async _doLoad(): Promise<void> {
|
private async _doLoad(): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/components-metadata.json');
|
const base = import.meta.env.BASE_URL || '/';
|
||||||
|
const response = await fetch(`${base}components-metadata.json`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load metadata: ${response.statusText}`);
|
throw new Error(`Failed to load metadata: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,8 @@ export class Esp32C3Simulator {
|
||||||
// @ts-expect-error kept for future use when more peripherals are emulated
|
// @ts-expect-error kept for future use when more peripherals are emulated
|
||||||
private async _loadRom(): Promise<void> {
|
private async _loadRom(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/boards/esp32c3-rom.bin');
|
const base = import.meta.env.BASE_URL || '/';
|
||||||
|
const resp = await fetch(`${base}boards/esp32c3-rom.bin`);
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const buf = await resp.arrayBuffer();
|
const buf = await resp.arrayBuffer();
|
||||||
this._romData = new Uint8Array(buf);
|
this._romData = new Uint8Array(buf);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue