diff --git a/frontend/src/components/simulator/PinOverlay.tsx b/frontend/src/components/simulator/PinOverlay.tsx index c45dc287..ac039e2e 100644 --- a/frontend/src/components/simulator/PinOverlay.tsx +++ b/frontend/src/components/simulator/PinOverlay.tsx @@ -71,12 +71,15 @@ export const PinOverlay: React.FC = ({ return null; } - // On touch devices, compute world-space size so the pin is at least - // TOUCH_MIN_SCREEN_PX on screen. On desktop, keep the original 12px. - const pinSize = isTouchDevice + // Visual indicator stays small (PIN_VISUAL world px). + // On touch devices the *hit area* is enlarged inversely to zoom so the + // 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) : PIN_VISUAL; - const pinHalf = pinSize / 2; + const hitHalf = hitSize / 2; + const visualHalf = PIN_VISUAL / 2; return (
= ({ }} style={{ position: 'absolute', - left: `${pinX - pinHalf}px`, - top: `${pinY - pinHalf}px`, - width: `${pinSize}px`, - height: `${pinSize}px`, - borderRadius: '3px', - backgroundColor: 'rgba(0, 200, 255, 0.8)', - border: '1.5px solid white', + left: `${pinX - hitHalf}px`, + top: `${pinY - hitHalf}px`, + width: `${hitSize}px`, + height: `${hitSize}px`, cursor: 'crosshair', pointerEvents: 'all', - transition: 'all 0.15s', touchAction: 'none', }} onMouseEnter={(e) => { - e.currentTarget.style.backgroundColor = 'rgba(0, 255, 100, 1)'; - e.currentTarget.style.transform = 'scale(1.4)'; + const dot = e.currentTarget.firstElementChild as HTMLElement; + if (dot) { + dot.style.backgroundColor = 'rgba(0, 255, 100, 1)'; + dot.style.transform = 'translate(-50%, -50%) scale(1.4)'; + } }} onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'rgba(0, 200, 255, 0.8)'; - e.currentTarget.style.transform = 'scale(1)'; + const dot = e.currentTarget.firstElementChild as HTMLElement; + if (dot) { + dot.style.backgroundColor = 'rgba(0, 200, 255, 0.8)'; + dot.style.transform = 'translate(-50%, -50%) scale(1)'; + } }} title={pin.name} - /> + > + {/* Small visible dot — stays at PIN_VISUAL regardless of zoom */} +
+
); })}
diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 48093808..57f8c781 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -16,6 +16,7 @@ import { PartSimulationRegistry } from '../../simulation/parts'; import { PinOverlay } from './PinOverlay'; import { isBoardComponent, boardPinToNumber } from '../../utils/boardPinMapping'; import { autoWireColor, WIRE_KEY_COLORS } from '../../utils/wireUtils'; +import { findClosestPin, getAllPinPositions } from '../../utils/pinPositionCalculator'; import { findWireNearPoint, getRenderedPoints, @@ -80,6 +81,10 @@ export const SimulatorCanvas = () => { const setSelectedWire = useSimulatorStore((s) => s.setSelectedWire); const removeWire = useSimulatorStore((s) => s.removeWire); 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); // Oscilloscope @@ -90,6 +95,13 @@ export const SimulatorCanvas = () => { const esp32CrashBoardId = useSimulatorStore((s) => s.esp32CrashBoardId); 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 const [showComponentPicker, setShowComponentPicker] = useState(false); const [registry] = useState(() => ComponentRegistry.getInstance()); @@ -198,8 +210,27 @@ export const SimulatorCanvas = () => { selectedWireIdRef.current = selectedWireId; const touchPassthroughRef = useRef(false); const touchOnPinRef = useRef(false); + const touchOnBannerRef = useRef(false); 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('idle'); + const wireAimingTimerRef = useRef | 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 const toWorld = useCallback((screenX: number, screenY: number) => { 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 useEffect(() => { initSimulator(); @@ -262,14 +331,18 @@ export const SimulatorCanvas = () => { // Reset per-gesture flags touchOnPinRef.current = false; touchPassthroughRef.current = false; + touchOnBannerRef.current = false; pinchStartDistRef.current = 0; if (e.touches.length === 2) { e.preventDefault(); - // Cancel wire in progress on two-finger gesture - if (wireInProgressRef.current) { - useSimulatorStore.getState().cancelWireCreation(); - } + // Preserve wire-in-progress during pinch-zoom — don't cancel it. + // The wire preview will freeze while pinching (touchMove only + // 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 isPanningRef.current = false; touchDraggedComponentIdRef.current = null; @@ -301,6 +374,12 @@ export const SimulatorCanvas = () => { 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 ── // (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis) // touch-action:none on .canvas-content already prevents browser scroll/zoom. @@ -317,15 +396,47 @@ export const SimulatorCanvas = () => { touchClickStartTimeRef.current = Date.now(); 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 ── - if (wireInProgressRef.current) { + // ── 3. Touch aiming for wire creation ── + // 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); - useSimulatorStore.getState().updateWireInProgress(world.x, world.y); - // Don't start pan/drag — let touchmove update wire preview, touchend add waypoint + if (aimPhase === 'aiming_end') { + useSimulatorStore.getState().updateWireInProgress(world.x, world.y + AIMING_OFFSET_Y); + } 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 ── const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null; const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null; @@ -367,7 +478,16 @@ export const SimulatorCanvas = () => { }; } } 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; panStartRef.current = { mouseX: touch.clientX, @@ -428,13 +548,31 @@ export const SimulatorCanvas = () => { return; } - // ── Wire preview: update position as finger moves ── - if (wireInProgressRef.current && !isPanningRef.current && !touchDraggedComponentIdRef.current) { + // ── Touch aiming: crosshair follows finger ── + 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); - 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; } + // 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) { // ── Single finger pan ── const dx = touch.clientX - panStartRef.current.mouseX; @@ -484,6 +622,11 @@ export const SimulatorCanvas = () => { e.preventDefault(); return; } + // Banner touch: let browser fire click on button + if (touchOnBannerRef.current) { + touchOnBannerRef.current = false; + return; // No preventDefault → click fires on button + } e.preventDefault(); @@ -514,18 +657,73 @@ export const SimulatorCanvas = () => { 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 ── let wasPanning = false; if (isPanningRef.current) { isPanningRef.current = false; setPan({ ...panRef.current }); 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 dx = changed.clientX - touchClickStartPosRef.current.x; const dy = changed.clientY - touchClickStartPosRef.current.y; @@ -541,14 +739,10 @@ export const SimulatorCanvas = () => { if (isShortTap) { if (touchId.startsWith('__board__:')) { - // Short tap on board → make it the active board const boardId = touchId.slice('__board__:'.length); useSimulatorStore.getState().setActiveBoardId(boardId); } 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 (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) { setSensorControlComponentId(touchId); @@ -567,15 +761,6 @@ export const SimulatorCanvas = () => { 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 ── if (isShortTap) { const now = Date.now(); @@ -1082,6 +1267,10 @@ export const SimulatorCanvas = () => { // Finish wire: connect to this pin finishWireCreation({ componentId, pinName, x, y }); trackCreateWire(); + // Reset aiming state + wireAimingPhaseRef.current = 'idle'; + setWireAiming(false); setAimPosition(null); + if (wireAimingTimerRef.current) { clearTimeout(wireAimingTimerRef.current); wireAimingTimerRef.current = null; } } else { // Start wire: auto-detect color from pin name startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName)); @@ -1091,9 +1280,23 @@ export const SimulatorCanvas = () => { // Keyboard handlers for wires useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // Escape → cancel in-progress wire - if (e.key === 'Escape' && wireInProgress) { - cancelWireCreation(); + // Escape → cancel in-progress wire or aiming + if (e.key === 'Escape' && (wireInProgress || wireAimingPhaseRef.current !== 'idle')) { + 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; } // Delete / Backspace → remove selected wire @@ -1114,7 +1317,7 @@ export const SimulatorCanvas = () => { window.addEventListener('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) useEffect(() => { @@ -1339,6 +1542,16 @@ export const SimulatorCanvas = () => {
+ {/* Undo / Redo wire */} +
+ + +
+ {/* Zoom controls */}
{/* Wire creation mode banner — visible on both desktop and mobile */} - {wireInProgress && ( + {(wireInProgress || wireAiming) && (
- Tap a pin to connect — tap canvas for waypoints - + {isTouchDevice + ? (wireAiming + ? '🎯 Geser ke pin, lepas untuk connect' + : 'Tahan lama untuk aim ke pin') + : 'Tap a pin to connect — tap canvas for waypoints'} +
)} + + {/* Wire selected banner — shows Delete + Undo buttons (essential for touch devices) */} + {!wireInProgress && !wireAiming && selectedWireId && ( +
+ Wire selected + + + {canUndoWire && } +
+ )} +
diff --git a/frontend/src/components/simulator/WireInProgressRenderer.tsx b/frontend/src/components/simulator/WireInProgressRenderer.tsx index 0fcb6922..1c098840 100644 --- a/frontend/src/components/simulator/WireInProgressRenderer.tsx +++ b/frontend/src/components/simulator/WireInProgressRenderer.tsx @@ -1,17 +1,24 @@ /** * WireInProgressRenderer — live preview while drawing a wire. * 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 type { WireInProgress } from '../../types/wire'; 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 { wireInProgress: WireInProgress; + /** When true, show the full-canvas crosshair guides (aiming mode on touch) */ + isAiming?: boolean; } -export const WireInProgressRenderer: React.FC = ({ wireInProgress }) => { +export const WireInProgressRenderer: React.FC = ({ wireInProgress, isAiming = false }) => { const { startEndpoint, waypoints, color, currentX, currentY } = wireInProgress; const path = generatePreviewPath( @@ -25,6 +32,26 @@ export const WireInProgressRenderer: React.FC = ({ wireInProgress }) => { return ( + {/* ── Crosshair: full-canvas alignment guides (only during aiming) ── */} + {isAiming && ( + <> + + + + )} + {/* Dark outline */} diff --git a/frontend/src/components/simulator/WireLayer.tsx b/frontend/src/components/simulator/WireLayer.tsx index bf7d264c..179c3dc6 100644 --- a/frontend/src/components/simulator/WireLayer.tsx +++ b/frontend/src/components/simulator/WireLayer.tsx @@ -22,6 +22,10 @@ interface WireLayerProps { onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void; /** Called when user starts dragging a handle via touch (passes segIndex) */ 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 = ({ @@ -30,6 +34,8 @@ export const WireLayer: React.FC = ({ segmentHandles, onHandleMouseDown, onHandleTouchStart, + isAiming, + zoom = 1, }) => { const wires = useSimulatorStore((s) => s.wires); const wireInProgress = useSimulatorStore((s) => s.wireInProgress); @@ -63,24 +69,29 @@ export const WireLayer: React.FC = ({ /> ))} - {/* Segment handles for the selected wire */} - {segmentHandles.map((handle) => ( - onHandleMouseDown(e, handle.segIndex)} - onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)} - /> - ))} + {/* Segment handles for the selected wire — scaled inversely to zoom for constant screen size */} + {segmentHandles.map((handle) => { + const baseR = isTouchDevice ? 14 : 7; + const r = baseR / zoom; + const sw = 2 / zoom; + return ( + onHandleMouseDown(e, handle.segIndex)} + onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)} + /> + ); + })} {wireInProgress && ( - + )} ); diff --git a/frontend/src/services/ComponentRegistry.ts b/frontend/src/services/ComponentRegistry.ts index ffff1f5e..8feb331c 100644 --- a/frontend/src/services/ComponentRegistry.ts +++ b/frontend/src/services/ComponentRegistry.ts @@ -55,7 +55,8 @@ export class ComponentRegistry { private async _doLoad(): Promise { 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) { throw new Error(`Failed to load metadata: ${response.statusText}`); } diff --git a/frontend/src/simulation/Esp32C3Simulator.ts b/frontend/src/simulation/Esp32C3Simulator.ts index 2a266919..9f4e6c1e 100644 --- a/frontend/src/simulation/Esp32C3Simulator.ts +++ b/frontend/src/simulation/Esp32C3Simulator.ts @@ -346,7 +346,8 @@ export class Esp32C3Simulator { // @ts-expect-error kept for future use when more peripherals are emulated private async _loadRom(): Promise { 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}`); const buf = await resp.arrayBuffer(); this._romData = new Uint8Array(buf);