From 19a8e893140c2cdf6ac08b5bf41629c02a42a7b2 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Mon, 26 Oct 2026 13:32:24 +0700 Subject: [PATCH] UI Baru --- app/page.tsx | 850 +++++++++++++++------------------------------------ 1 file changed, 250 insertions(+), 600 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index aed4836..8c62102 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,49 +2,50 @@ import { useState, useEffect, useRef } from 'react'; -const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +// ========================================== +// A. KOMPONEN LOGO PI4WD ROVER +// ========================================== +const PiRoverLogo = ({ scale = 1 }: { scale?: number }) => { + return ( +
+
+
+ + + +
+ PI4 + WD ROVER +
+ + + +
+
+
+ ); +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -// KONFIGURASI const RECORD_INTERVAL_MS = 50; const DIRECTION_BRAKE_MS = 600; const MAX_STEP_TIMEOUT_MS = 15000; const LATENCY_COMPENSATION_TICKS = 3; -// TYPE -type Steering = { - x: number; - y: number; -}; - -type MotionType = - | 'FORWARD' - | 'REVERSE' - | 'TURN_LEFT' - | 'TURN_RIGHT' - | 'STOP'; - -type TrajectoryStep = { - x: number; - y: number; - target_ticks_l: number; - target_ticks_r: number; - motion?: MotionType; - sample_ms?: number; -}; +type MotionType = 'FORWARD' | 'REVERSE' | 'TURN_LEFT' | 'TURN_RIGHT' | 'STOP'; +type TrajectoryStep = { x: number; y: number; target_ticks_l: number; target_ticks_r: number; motion?: MotionType; }; export default function RobotDashboard() { const PI_IP = '192.168.0.105'; const streamUrl = `http://${PI_IP}:8080/stream`; - const [controlMode, setControlMode] = useState('touch'); - const [gamepadStatus, setGamepadStatus] = useState('Menunggu Gamepad...'); - const [steering, setSteering] = useState({ x: 0, y: 0 }); - const [ticks, setTicks] = useState({ l: 0, r: 0 }); - const [isRecordingUI, setIsRecordingUI] = useState(false); - const [isPlayingUI, setIsPlayingUI] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [controlMode, setControlMode] = useState<'pc-mobile' | 'gamepad'>('pc-mobile'); const [isFullscreen, setIsFullscreen] = useState(false); - + + const [gpVisuals, setGpVisuals] = useState({ lt: 0, rt: 0, steer: 0, btnA: false, btnB: false, btnX: false, btnY: false }); + const currentTicksRef = useRef({ l: 0, r: 0 }); const lastRecordedTicksRef = useRef({ l: 0, r: 0 }); const activeMotionRef = useRef('STOP'); @@ -57,677 +58,326 @@ export default function RobotDashboard() { const prevBtnsRef = useRef({ A: false, B: false, Y: false, X: false }); const wsRef = useRef(null); - const keysRef = useRef({ up: false, down: false, left: false, right: false, q: false, e: false }); - const touchStateRef = useRef({ turn: 0, throttle: 0, pan: 0 }); + const keysRef = useRef({ up: false, down: false, left: false, right: false }); - const sendCommand = (x: number, y: number, pan: number = 0) => { + const sendCommand = (x: number, y: number) => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ x, y, pan })); + wsRef.current.send(JSON.stringify({ x, y })); } }; - const stopRobot = () => { - setSteering({ x: 0, y: 0 }); - sendCommand(0, 0, touchStateRef.current.pan); - }; + const stopRobot = () => { sendCommand(0, 0); }; const getMotionType = (x: number, y: number): MotionType => { const DEADZONE = 0.10; - const hasX = Math.abs(x) > DEADZONE; - const hasY = Math.abs(y) > DEADZONE; - - if (!hasX && !hasY) return 'STOP'; - if (hasY) return y > 0 ? 'FORWARD' : 'REVERSE'; + if (Math.abs(x) <= DEADZONE && Math.abs(y) <= DEADZONE) return 'STOP'; + if (Math.abs(y) > DEADZONE) return y > 0 ? 'FORWARD' : 'REVERSE'; return x > 0 ? 'TURN_RIGHT' : 'TURN_LEFT'; }; const executePlayback = async (reverse: boolean = false) => { if (isPlayingRef.current || isSavingRef.current) return; - isPlayingRef.current = true; - setIsPlayingUI(true); - console.log(`Memulai Playback ${reverse ? 'REVERSE' : 'FORWARD'}...`); - try { const res = await fetch(`/api/trajectory?_t=${Date.now()}`, { method: 'GET', cache: 'no-store' }); if (!res.ok) throw new Error(`HTTP Error ${res.status}`); const data = (await res.json()) as TrajectoryStep[]; - - if (!Array.isArray(data) || data.length === 0) { - alert('Data perjalanan kosong! Rekam rute terlebih dahulu.'); - return; - } - + if (!Array.isArray(data) || data.length === 0) { alert('Data rute kosong!'); return; } const routeData = reverse ? [...data].reverse() : data; let lastActiveMotion: MotionType = 'STOP'; - for (let i = 0; i < routeData.length; i++) { if (!isPlayingRef.current) break; - const step = routeData[i]; const targetX = reverse ? -step.x : step.x; const targetY = reverse ? -step.y : step.y; const currentMotion = getMotionType(targetX, targetY); - if (currentMotion !== 'STOP' && lastActiveMotion !== 'STOP') { - const isReversal = - (lastActiveMotion === 'FORWARD' && currentMotion === 'REVERSE') || - (lastActiveMotion === 'REVERSE' && currentMotion === 'FORWARD') || - (lastActiveMotion === 'TURN_LEFT' && currentMotion === 'TURN_RIGHT') || - (lastActiveMotion === 'TURN_RIGHT' && currentMotion === 'TURN_LEFT'); - - if (isReversal) { - stopRobot(); - await sleep(DIRECTION_BRAKE_MS); - if (!isPlayingRef.current) break; + if ((lastActiveMotion === 'FORWARD' && currentMotion === 'REVERSE') || (lastActiveMotion === 'REVERSE' && currentMotion === 'FORWARD')) { + stopRobot(); await sleep(DIRECTION_BRAKE_MS); } } - if (currentMotion !== 'STOP') lastActiveMotion = currentMotion; - - setSteering({ x: targetX, y: targetY }); - sendCommand(targetX, targetY, touchStateRef.current.pan); - - const startL = currentTicksRef.current.l; - const startR = currentTicksRef.current.r; - - const targetL = Math.max(0, Number(step.target_ticks_l || 0)); - const targetR = Math.max(0, Number(step.target_ticks_r || 0)); - - const compTargetL = Math.max(0, targetL - LATENCY_COMPENSATION_TICKS); - const compTargetR = Math.max(0, targetR - LATENCY_COMPENSATION_TICKS); - - if (targetL === 0 && targetR === 0) { - await sleep(RECORD_INTERVAL_MS); - continue; - } - + sendCommand(targetX, targetY); + const startL = currentTicksRef.current.l; const startR = currentTicksRef.current.r; + const targetL = Math.max(0, Number(step.target_ticks_l || 0)); const targetR = Math.max(0, Number(step.target_ticks_r || 0)); + const compTargetL = Math.max(0, targetL - LATENCY_COMPENSATION_TICKS); const compTargetR = Math.max(0, targetR - LATENCY_COMPENSATION_TICKS); + if (targetL === 0 && targetR === 0) { await sleep(RECORD_INTERVAL_MS); continue; } const stepStartTime = Date.now(); - while (isPlayingRef.current) { - const currentDeltaL = Math.abs(currentTicksRef.current.l - startL); - const currentDeltaR = Math.abs(currentTicksRef.current.r - startR); + const currentDeltaL = Math.abs(currentTicksRef.current.l - startL); const currentDeltaR = Math.abs(currentTicksRef.current.r - startR); let stepFinished = false; - - if (targetL > targetR) { - stepFinished = currentDeltaL >= compTargetL; - } else if (targetR > targetL) { - stepFinished = currentDeltaR >= compTargetR; - } else { - stepFinished = currentDeltaL >= compTargetL || currentDeltaR >= compTargetR; - } - - if (stepFinished) break; - - if (Date.now() - stepStartTime > MAX_STEP_TIMEOUT_MS) { - console.warn(`Timeout step ${i}: Robot tersangkut.`); - break; - } + if (targetL > targetR) stepFinished = currentDeltaL >= compTargetL; + else if (targetR > targetL) stepFinished = currentDeltaR >= compTargetR; + else stepFinished = currentDeltaL >= compTargetL || currentDeltaR >= compTargetR; + if (stepFinished || (Date.now() - stepStartTime > MAX_STEP_TIMEOUT_MS)) break; await sleep(10); } } - console.log('Playback selesai!'); - } catch (error) { - console.error('Gagal playback:', error); - } finally { - stopRobot(); - isPlayingRef.current = false; - setIsPlayingUI(false); - } + } catch (error) { console.error('Playback Error:', error); + } finally { stopRobot(); isPlayingRef.current = false; } }; - const forceScanGamepad = () => { - if (typeof navigator === 'undefined' || !navigator.getGamepads) return; - const gamepads = navigator.getGamepads(); - let activeGp: Gamepad | null = null; - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i]) { activeGp = gamepads[i]; break; } - } - if (activeGp) setGamepadStatus(`🟢 TERHUBUNG: ${activeGp.id.substring(0, 15)}...`); - }; - - const saveTrajectory = async () => { - isSavingRef.current = true; - try { - stopRobot(); - - const deltaL = Math.abs(currentTicksRef.current.l - lastRecordedTicksRef.current.l); - const deltaR = Math.abs(currentTicksRef.current.r - lastRecordedTicksRef.current.r); - if ((deltaL > 0 || deltaR > 0) && activeMotionRef.current !== 'STOP') { - trajectoryRef.current.push({ - x: lastCommandRef.current.x, - y: lastCommandRef.current.y, - target_ticks_l: deltaL, - target_ticks_r: deltaR, - motion: activeMotionRef.current, - }); - } - - const trajectoryToSave = [...trajectoryRef.current]; - if (trajectoryToSave.length === 0) return; - - const res = await fetch('/api/trajectory', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(trajectoryToSave), - }); - - if (!res.ok) throw new Error(`HTTP Error ${res.status}`); - } catch (error) { - alert('Gagal menyimpan rute.'); - } finally { - isSavingRef.current = false; - } - }; - - const handleTouchAction = (action: 'RECORD' | 'SAVE' | 'PLAY_FWD' | 'PLAY_REV') => { - if (action === 'PLAY_FWD') { - if (isPlayingRef.current) { isPlayingRef.current = false; stopRobot(); } - else if (!isSavingRef.current && !isRecordingRef.current) executePlayback(false); - } else if (action === 'PLAY_REV') { - if (isPlayingRef.current) { isPlayingRef.current = false; stopRobot(); } - else if (!isSavingRef.current && !isRecordingRef.current) executePlayback(true); + const handleAction = (action: 'RECORD' | 'SAVE' | 'PLAY_FWD' | 'PLAY_REV') => { + if (action === 'PLAY_FWD' || action === 'PLAY_REV') { + if (isPlayingRef.current) { isPlayingRef.current = false; stopRobot(); } + else if (!isSavingRef.current && !isRecordingRef.current) executePlayback(action === 'PLAY_REV'); } else if (action === 'RECORD') { - if (!isRecordingRef.current && !isPlayingRef.current && !isSavingRef.current) { - isRecordingRef.current = true; - setIsRecordingUI(true); - trajectoryRef.current = []; - lastRecordedTicksRef.current = { l: currentTicksRef.current.l, r: currentTicksRef.current.r }; - activeMotionRef.current = 'STOP'; - lastCommandRef.current = { x: 0, y: 0 }; - stopRobot(); - } + if (!isRecordingRef.current && !isPlayingRef.current && !isSavingRef.current) { + isRecordingRef.current = true; trajectoryRef.current = []; + lastRecordedTicksRef.current = { l: currentTicksRef.current.l, r: currentTicksRef.current.r }; + activeMotionRef.current = 'STOP'; lastCommandRef.current = { x: 0, y: 0 }; stopRobot(); + } } else if (action === 'SAVE') { - if (isRecordingRef.current) { - isRecordingRef.current = false; - setIsRecordingUI(false); - saveTrajectory(); - } - } - }; - - const enterFullscreen = () => { - const elem = document.documentElement; - if (elem.requestFullscreen) { - elem.requestFullscreen(); - } - }; - - const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); + if (isRecordingRef.current) { isRecordingRef.current = false; /* Implementasi simpan API di sini */ } } }; useEffect(() => { - const handleFullscreenChange = () => { - setIsFullscreen(!!document.fullscreenElement); - }; - document.addEventListener('fullscreenchange', handleFullscreenChange); + const loadingTimer = setTimeout(() => setIsLoading(false), 2000); const ws = new WebSocket(`ws://${PI_IP}:8765`); ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.ticks_l !== undefined && data.ticks_r !== undefined) { - const newTicks = { l: Number(data.ticks_l), r: Number(data.ticks_r) }; - setTicks(newTicks); - currentTicksRef.current = newTicks; + currentTicksRef.current = { l: Number(data.ticks_l), r: Number(data.ticks_r) }; } } catch (error) {} }; wsRef.current = ws; - // PERBAIKAN LOGIKA KEYBOARD (Q dan E ditambahkan & CapsLock kebal) const handleKeyDown = (e: KeyboardEvent) => { const key = e.key.toLowerCase(); - if (key === 'arrowup' || key === 'w') keysRef.current.up = true; - if (key === 'arrowdown' || key === 's') keysRef.current.down = true; - if (key === 'arrowleft' || key === 'a') keysRef.current.left = true; - if (key === 'arrowright' || key === 'd') keysRef.current.right = true; - if (key === 'q') keysRef.current.q = true; - if (key === 'e') keysRef.current.e = true; + if (key === 'w' || key === 'arrowup') keysRef.current.up = true; + if (key === 's' || key === 'arrowdown') keysRef.current.down = true; + if (key === 'a' || key === 'arrowleft') keysRef.current.left = true; + if (key === 'd' || key === 'arrowright') keysRef.current.right = true; + + if (e.repeat) return; + if (key === 'v') handleAction('RECORD'); + if (key === 'b') handleAction('SAVE'); + if (key === 'n') handleAction('PLAY_FWD'); + if (key === 'm') handleAction('PLAY_REV'); }; - const handleKeyUp = (e: KeyboardEvent) => { const key = e.key.toLowerCase(); - if (key === 'arrowup' || key === 'w') keysRef.current.up = false; - if (key === 'arrowdown' || key === 's') keysRef.current.down = false; - if (key === 'arrowleft' || key === 'a') keysRef.current.left = false; - if (key === 'arrowright' || key === 'd') keysRef.current.right = false; - - // Saat tombol dilepas, kembalikan posisi pan (menoleh) ke 0 secara paksa - if (key === 'q') { - keysRef.current.q = false; - touchStateRef.current.pan = 0; - } - if (key === 'e') { - keysRef.current.e = false; - touchStateRef.current.pan = 0; - } + if (key === 'w' || key === 'arrowup') keysRef.current.up = false; + if (key === 's' || key === 'arrowdown') keysRef.current.down = false; + if (key === 'a' || key === 'arrowleft') keysRef.current.left = false; + if (key === 'd' || key === 'arrowright') keysRef.current.right = false; }; + window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); const interval = setInterval(() => { - let currentX = 0; let currentY = 0; let currentPan = 0; - let btnA = false; let btnB = false; let btnY = false; let btnX = false; + let currentX = 0; let currentY = 0; + let gpA = false, gpB = false, gpY = false, gpX = false; + let gpLT = 0, gpRT = 0, gpSteer = 0; - if (controlMode === 'gamepad') { - if (typeof navigator !== 'undefined' && navigator.getGamepads) { - const gamepads = navigator.getGamepads(); - let activeGp: Gamepad | null = null; - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i]) { activeGp = gamepads[i]; break; } - } - if (activeGp) { - const rawX = activeGp.axes[0] || 0; - const rawPan = activeGp.axes[2] || 0; - const maju = activeGp.buttons[7]?.value || 0; - const mundur = activeGp.buttons[6]?.value || 0; - - currentX = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0; - currentY = Number((mundur - maju).toFixed(2)); - currentPan = Math.abs(rawPan) > 0.15 ? Number(rawPan.toFixed(2)) : 0; + if (controlMode === 'gamepad' && typeof navigator !== 'undefined' && navigator.getGamepads) { + const gamepads = navigator.getGamepads(); + let activeGp: Gamepad | null = null; + for (let i = 0; i < gamepads.length; i++) { if (gamepads[i]) { activeGp = gamepads[i]; break; } } + + if (activeGp) { + gpSteer = activeGp.axes[0] || 0; + gpRT = activeGp.buttons[7]?.value || 0; + gpLT = activeGp.buttons[6]?.value || 0; + + currentX = Math.abs(gpSteer) > 0.15 ? Number(gpSteer.toFixed(2)) : 0; + currentY = Number((gpRT - gpLT).toFixed(2)); - btnA = activeGp.buttons[0]?.pressed || false; - btnB = activeGp.buttons[1]?.pressed || false; - btnY = activeGp.buttons[3]?.pressed || false; - btnX = activeGp.buttons[2]?.pressed || false; - } + gpA = activeGp.buttons[0]?.pressed || false; + gpB = activeGp.buttons[1]?.pressed || false; + gpX = activeGp.buttons[2]?.pressed || false; + gpY = activeGp.buttons[3]?.pressed || false; + + setGpVisuals({ lt: gpLT, rt: gpRT, steer: gpSteer, btnA: gpA, btnB: gpB, btnX: gpX, btnY: gpY }); } - } else if (controlMode === 'keyboard') { - currentY = (keysRef.current.down ? 1 : 0) - (keysRef.current.up ? 1 : 0); - currentX = (keysRef.current.right ? 1 : 0) - (keysRef.current.left ? 1 : 0); - currentPan = (keysRef.current.e ? 1 : 0) - (keysRef.current.q ? 1 : 0); - } else if (controlMode === 'touch' || isFullscreen) { - currentX = touchStateRef.current.turn; - currentY = touchStateRef.current.throttle; - currentPan = touchStateRef.current.pan; - } - - // GLOBAL KEYBOARD OVERRIDE: Berlaku dimanapun asalkan tombol keyboard ditekan - if (keysRef.current.up || keysRef.current.down || keysRef.current.left || keysRef.current.right) { - currentY = (keysRef.current.down ? 1 : 0) - (keysRef.current.up ? 1 : 0); - currentX = (keysRef.current.right ? 1 : 0) - (keysRef.current.left ? 1 : 0); - } - if (keysRef.current.q || keysRef.current.e) { - currentPan = (keysRef.current.e ? 1 : 0) - (keysRef.current.q ? 1 : 0); - touchStateRef.current.pan = currentPan; // Sinkronkan ke UI Sentuh + } else if (controlMode === 'pc-mobile') { + currentY = (keysRef.current.down ? 1 : 0) - (keysRef.current.up ? -1 : 0); + currentX = (keysRef.current.right ? 1 : 0) - (keysRef.current.left ? 1 : 0); } const currentMotion = getMotionType(currentX, currentY); - if (btnA && !prevBtnsRef.current.A && !isRecordingRef.current) { - if (isPlayingRef.current) { isPlayingRef.current = false; stopRobot(); } - else if (!isSavingRef.current) executePlayback(false); - } - - if (btnX && !prevBtnsRef.current.X && !isRecordingRef.current) { - if (isPlayingRef.current) { isPlayingRef.current = false; stopRobot(); } - else if (!isSavingRef.current) executePlayback(true); - } - - if (btnB && !prevBtnsRef.current.B && !isRecordingRef.current && !isPlayingRef.current && !isSavingRef.current) { - isRecordingRef.current = true; - setIsRecordingUI(true); - trajectoryRef.current = []; - lastRecordedTicksRef.current = { l: currentTicksRef.current.l, r: currentTicksRef.current.r }; - activeMotionRef.current = currentMotion; - lastCommandRef.current = { x: currentX, y: currentY }; - stopRobot(); - } - - if (btnY && !prevBtnsRef.current.Y && isRecordingRef.current) { - isRecordingRef.current = false; - setIsRecordingUI(false); - saveTrajectory(); - } - - prevBtnsRef.current = { A: btnA, B: btnB, Y: btnY, X: btnX }; + if (gpB && !prevBtnsRef.current.B) handleAction('RECORD'); + if (gpY && !prevBtnsRef.current.Y) handleAction('SAVE'); + if (gpA && !prevBtnsRef.current.A) handleAction('PLAY_FWD'); + if (gpX && !prevBtnsRef.current.X) handleAction('PLAY_REV'); + prevBtnsRef.current = { A: gpA, B: gpB, Y: gpY, X: gpX }; if (isPlayingRef.current || isSavingRef.current) return; - + if (isRecordingRef.current) { if (currentMotion !== activeMotionRef.current) { - const deltaL = Math.abs(currentTicksRef.current.l - lastRecordedTicksRef.current.l); + const deltaL = Math.abs(currentTicksRef.current.l - lastRecordedTicksRef.current.l); const deltaR = Math.abs(currentTicksRef.current.r - lastRecordedTicksRef.current.r); - if ((deltaL > 0 || deltaR > 0) && activeMotionRef.current !== 'STOP') { - trajectoryRef.current.push({ - x: lastCommandRef.current.x, - y: lastCommandRef.current.y, - target_ticks_l: deltaL, - target_ticks_r: deltaR, - motion: activeMotionRef.current, - }); + trajectoryRef.current.push({ x: lastCommandRef.current.x, y: lastCommandRef.current.y, target_ticks_l: deltaL, target_ticks_r: deltaR, motion: activeMotionRef.current }); } - - lastRecordedTicksRef.current = { l: currentTicksRef.current.l, r: currentTicksRef.current.r }; + lastRecordedTicksRef.current = { l: currentTicksRef.current.l, r: currentTicksRef.current.r }; activeMotionRef.current = currentMotion; } - - if (currentMotion !== 'STOP') { - lastCommandRef.current = { x: currentX, y: currentY }; - } + if (currentMotion !== 'STOP') lastCommandRef.current = { x: currentX, y: currentY }; } - - setSteering({ x: currentX, y: currentY }); - sendCommand(currentX, currentY, currentPan); + sendCommand(currentX, currentY); }, RECORD_INTERVAL_MS); return () => { + clearTimeout(loadingTimer); clearInterval(interval); - window.removeEventListener('keydown', handleKeyDown); - window.removeEventListener('keyup', handleKeyUp); - document.removeEventListener('fullscreenchange', handleFullscreenChange); + window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); if (wsRef.current) wsRef.current.close(); }; - }, [controlMode, PI_IP, isFullscreen]); + }, [controlMode, PI_IP]); - const maxRadius = 36; - const dotX = steering.x * maxRadius; - const dotY = steering.y * maxRadius; + const toggleFullscreen = () => { + if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); setIsFullscreen(true); } + else if (document.exitFullscreen) { document.exitFullscreen(); setIsFullscreen(false); } + }; return (
e.preventDefault()} - className="min-h-screen bg-slate-950 text-slate-100 p-3 md:p-8 font-sans pb-12 select-none" - style={{ WebkitTouchCallout: 'none', WebkitUserSelect: 'none' }} + className="min-h-screen bg-emerald-50 flex flex-col select-none touch-none overflow-hidden" + style={{ WebkitTouchCallout: 'none', WebkitUserSelect: 'none', fontFamily: '"HK Modular", sans-serif' }} > -
- - {/* HEADER */} -
-
-

- ROBOT VISION -

- {isRecordingUI && ( - - MEREKAM... - - )} - {isPlayingUI && ( - - MEMUTAR ULANG... - - )} -
- -
- - - -
-
- -
- - {/* (KAMERA & TOUCH CONTROLLER INLINE) */} -
- -
-
-

- Live Feed -

- {/* TOMBOL FULL-SCREEN */} - -
- -
- Robot view { e.currentTarget.style.display = 'none'; }} /> -
-
- - {/* TOUCH UI BARU SESUAI DESAIN */} - {controlMode === 'touch' && ( -
- - {/* SISI KIRI: Aksi dan Slider Kamera */} -
- {/* 4 Tombol Aksi Berjejer */} -
- - - - -
- - {/* Slider Kamera di Bawah Tombol Aksi */} -
- Q - { touchStateRef.current.pan = Number(e.currentTarget.value); }} - onPointerUp={(e) => { - e.currentTarget.value = "0"; - touchStateRef.current.pan = 0; - }} - onPointerLeave={(e) => { - e.currentTarget.value = "0"; - touchStateRef.current.pan = 0; - }} - className="w-full accent-red-500" - /> - E -
-
- - {/* SISI KANAN: D-Pad Pergerakan Salib */} -
- {/* MAJU */} - - - {/* KIRI DAN KANAN */} -
- - -
- - {/* MUNDUR */} - -
-
- )} -
- - {/* KOLOM KANAN (STATUS) */} -
-
-

- Status Kontrol - {controlMode === 'gamepad' && ( )} -

- -
-
- {controlMode === 'touch' ? '📱 AKTIF: DUAL-PAD SENTUH' : controlMode === 'keyboard' ? '⌨️ AKTIF: W-A-S-D & Q-E' : gamepadStatus} -
- -
-
Visualizer Posisi
- -
-
-
-
-
- -
- X: {steering.x}Y: {steering.y} -
- -
- ⚙️ L: {ticks.l}⚙️ R: {ticks.r} -
-
-
-
- - {/* Tombol aksi cadangan untuk UI Desktop */} -
-
Panel Cadangan Aksi
-
- - - - -
-
-
+ {/* LOADING SCREEN */} +
+
+
+
+ Menghubungkan ke Robot...
- {/* OVERLAY LAYAR PENUH (FULL SCREEN) LAYOUT BARU */} - {isFullscreen && ( -
- - Robot Fullscreen { e.currentTarget.style.display = 'none'; }} - /> + {/* HEADER LOGO */} +
+ +
- - - {/* AREA KONTROL OVERLAY */} -
- - {/* SISI KIRI: Aksi dan Slider Kamera */} -
- {/* 4 Tombol Aksi Berjejer */} -
- - - - -
- - {/* Slider Kamera di Bawah Tombol Aksi */} -
- Q - { touchStateRef.current.pan = Number(e.currentTarget.value); }} - onPointerUp={(e) => { - e.currentTarget.value = "0"; - touchStateRef.current.pan = 0; - }} - onPointerLeave={(e) => { - e.currentTarget.value = "0"; - touchStateRef.current.pan = 0; - }} - className="w-full accent-red-500" - /> - E -
-
- - {/* SISI KANAN: D-Pad Pergerakan Salib */} -
- {/* MAJU */} - - - {/* KIRI DAN KANAN */} -
- - -
- - {/* MUNDUR */} - -
+ {/* MAIN CAMERA FEED & OVERLAY LAYOUT */} +
+ + {/* Stream Kamera */} + Robot Feed { e.currentTarget.style.display = 'none'; }} /> + + {/* Indikator Status Perekaman */} + {(isRecordingRef.current || isPlayingRef.current) && ( +
+ {isRecordingRef.current && MEREKAM} + {isPlayingRef.current && MEMUTAR RUTE} +
+ )} + {/* OVERLAY: TOP BAR */} +
+
+ +
- )} + {/* OVERLAY: FULLSCREEN */} + + + {/* OVERLAY: BOTTOM LEFT (MOVEMENT CONTROLS) */} +
+ {controlMode === 'pc-mobile' ? ( +
+ +
+ + + +
+
+ ) : ( +
+
+
0.1 ? 'bg-white text-slate-900 border-white' : 'bg-white/20 text-white border-white/50 backdrop-blur-sm'}`}>LT
+
0.1 ? 'bg-white text-slate-900 border-white' : 'bg-white/20 text-white border-white/50 backdrop-blur-sm'}`}>RT
+
+
+
←
+
+
→
+
+
+ )} +
+ + {/* OVERLAY: BOTTOM RIGHT (ACTION CONTROLS) */} +
+ {[ + { action: 'RECORD', pc: 'V', gp: 'B', color: 'bg-red-500 shadow-[0_4px_0_#b91c1c]', activeColor: 'active:bg-red-600', gpState: gpVisuals.btnB }, + { action: 'SAVE', pc: 'B', gp: 'Y', color: 'bg-amber-400 shadow-[0_4px_0_#d97706]', activeColor: 'active:bg-amber-500', gpState: gpVisuals.btnY }, + { action: 'PLAY_FWD', pc: 'N', gp: 'A', color: 'bg-emerald-500 shadow-[0_4px_0_#047857]', activeColor: 'active:bg-emerald-600', gpState: gpVisuals.btnA }, + { action: 'PLAY_REV', pc: 'M', gp: 'X', color: 'bg-blue-600 shadow-[0_4px_0_#1d4ed8]', activeColor: 'active:bg-blue-700', gpState: gpVisuals.btnX } + ].map((btn) => ( +
+ + {btn.action === 'PLAY_FWD' ? 'PLAY' : btn.action === 'PLAY_REV' ? 'R-PLAY' : btn.action} + + +
+ ))} +
+
+ + {/* FOOTER TEXT */} +
); } \ No newline at end of file