From eb7b7f53d17c3c8906eaa8aae6d87a847dcb2ff6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 18:05:01 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/XRHudInWorld.tsx | 157 +++++++++++--------------- 1 file changed, 63 insertions(+), 94 deletions(-) diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 51599cd..a202b7a 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -1,10 +1,9 @@ -import { useRef, useState, useEffect } from 'react'; -import { forwardRef } from 'react'; +import { useRef, useState, forwardRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { useXRInputSourceState } from '@react-three/xr'; import * as THREE from 'three'; -import { XR3DButton, XR3DPanel } from './XRPanel3D'; import { Text } from '@react-three/drei'; +import { XR3DButton, XR3DPanel } from './XRPanel3D'; import { useModelStore, SCALE_PRESETS } from '@/stores/useModelStore'; import { getBroadcastSource } from '@/lib/webrtc/broadcastSource'; @@ -29,11 +28,26 @@ const POS_STEP = 0.001; const ROT_STEP = 1; const SCALE_LABELS = ['1:50', '1:20', '1:10', '1:1']; -/** - * In-world AR HUD: a floating panel anchored in front of the user with all - * tools, plus a tiny "wrist toggle" attached to the left controller. Works - * inside the WebXR passthrough, where DOM overlays are invisible. - */ +const WristToggle = forwardRef void }>( + function WristToggle({ open, onToggle }, ref) { + return ( + + + + + + { e.stopPropagation(); onToggle(); }}> + + + + + {open ? '✕ Fechar' : '☰ Menu'} + + + ); + }, +); + export function XRHudInWorld(props: XRHudInWorldProps) { const { camera } = useThree(); const leftCtrl = useXRInputSourceState('controller', 'left'); @@ -44,10 +58,10 @@ export function XRHudInWorld(props: XRHudInWorldProps) { const wristRef = useRef(null); const targetPos = useRef(new THREE.Vector3()); const targetQuat = useRef(new THREE.Quaternion()); - - // Toggle panel via left A button (buttons[4] on Quest Touch) const lastABtn = useRef(false); + useFrame(() => { + // Toggle via left A button (Quest Touch buttons[4]) const gp = leftCtrl?.inputSource?.gamepad; if (gp) { const aBtn = gp.buttons[4]; @@ -56,18 +70,17 @@ export function XRHudInWorld(props: XRHudInWorldProps) { lastABtn.current = pressed; } - // Position the floating panel ~70cm in front of the camera, with damping. + // Floating panel ~70cm in front of head, yaw-locked, damped if (panelRef.current && open) { const fwd = new THREE.Vector3(0, -0.05, -0.7).applyQuaternion(camera.quaternion); targetPos.current.copy(camera.position).add(fwd); - // Face camera (yaw only, keep panel upright) const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ'); targetQuat.current.setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ')); panelRef.current.position.lerp(targetPos.current, 0.12); panelRef.current.quaternion.slerp(targetQuat.current, 0.18); } - // Position wrist toggle above the left controller + // Wrist toggle follows left controller if (wristRef.current) { const ctrlObj = leftCtrl?.object; if (ctrlObj) { @@ -86,7 +99,7 @@ export function XRHudInWorld(props: XRHudInWorldProps) { return ( <> - setOpen((v) => !v)} /> + setOpen((v) => !v)} /> {open && ( @@ -96,43 +109,17 @@ export function XRHudInWorld(props: XRHudInWorldProps) { ); } -const WristToggleReal = forwardRef void }>( - function WristToggleReal({ open, onToggle }, ref) { - return ( - - { e.stopPropagation(); onToggle(); }} - onPointerOver={(e) => e.stopPropagation()} - > - - - - - - - - - {open ? '✕ Fechar' : '☰ Menu'} - - - ); - } -); - -// ───────────────────────────────────────────────────────────────────────────── function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => void } & XRHudInWorldProps) { const tabs: { id: Tab; label: string; icon: string }[] = [ - { id: 'scene', label: 'Cena', icon: '🧩' }, - { id: 'tools', label: 'Ferram.', icon: '🛠' }, - { id: 'transform', label: 'Mover', icon: '↔' }, - { id: 'inspection', label: 'Inspeção', icon: '✓' }, - { id: 'share', label: 'Compart.', icon: '📡' }, + { id: 'scene', label: 'Cena', icon: '🧩' }, + { id: 'tools', label: 'Ferram.', icon: '🛠' }, + { id: 'transform', label: 'Mover', icon: '↔' }, + { id: 'inspection', label: 'Inspeção', icon: '✓' }, + { id: 'share', label: 'Compart.', icon: '📡' }, ]; - const W = 0.5, H = 0.36; return ( - {/* Title */} TrackSteelXR · HUD AR @@ -142,23 +129,15 @@ function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => vo Botão A (esq) abre/fecha - {/* Tabs row */} {tabs.map((t, i) => ( - setTab(t.id)} - fontSize={0.0075} - /> + setTab(t.id)} fontSize={0.0075} /> ))} - {/* Content area */} {tab === 'scene' && } {tab === 'tools' && } @@ -181,8 +160,8 @@ function SceneTab() { if (models.length === 0) { return ( - - Nenhuma peça na cena. Saia do AR para carregar arquivos. + + Nenhuma peça na cena. Saia do AR para carregar arquivos no Viewer. ); } @@ -190,19 +169,17 @@ function SceneTab() { return ( - Toque para selecionar peça ativa + Toque no nome para tornar a peça ativa {models.slice(0, 5).map((m, i) => { const y = 0.085 - i * 0.04; const isActive = m.id === activeId; return ( - {/* Color dot */} - {/* Name (tappable to make active) */} { e.stopPropagation(); setActive(m.id); }}> @@ -214,7 +191,6 @@ function SceneTab() { {m.fileName} - {/* Action buttons */} toggleVisible(m.id)} /> @@ -248,29 +224,27 @@ function ToolsTab(p: XRHudInWorldProps) { return ( - {/* Render mode */} Render setRenderMode('solid')} /> setRenderMode('wireframe')} /> - setRenderMode('edges')} /> - {/* Toggles */} Toggles setShowGrid(!showGrid)} /> setMeasureMode(!measureMode)} /> - - - {/* Opacity slider buttons */} Opacidade {Math.round(opacity * 100)}% @@ -285,12 +259,11 @@ function ToolsTab(p: XRHudInWorldProps) { setOpacity(1)} /> - {/* Scale presets */} Escala {scaleRatio.label} {SCALE_LABELS.map((lbl, i) => { - const preset = SCALE_PRESETS.find(p => p.label === lbl)!; + const preset = SCALE_PRESETS.find((sp) => sp.label === lbl)!; return ( s.setFineTuning); const reset = useModelStore((s) => s.resetFineTuning); - const Row = ({ label, axis, isRot, y }: { label: string; axis: 'posX'|'posY'|'posZ'|'rotX'|'rotY'|'rotZ'; isRot: boolean; y: number }) => { + const Row = ({ + label, axis, isRot, y, + }: { label: string; axis: 'posX'|'posY'|'posZ'|'rotX'|'rotY'|'rotZ'; isRot: boolean; y: number }) => { const step = isRot ? ROT_STEP : POS_STEP; const val = fineTuning[axis]; const display = isRot ? `${val.toFixed(1)}°` : `${(val * 1000).toFixed(1)}mm`; @@ -317,16 +292,16 @@ function TransformTab() { {label} setFineTuning({ [axis]: val - step * 10 } as Partial)} /> + onClick={() => setFineTuning({ [axis]: val - step * 10 })} /> setFineTuning({ [axis]: val - step } as Partial)} /> + onClick={() => setFineTuning({ [axis]: val - step })} /> {display} setFineTuning({ [axis]: val + step } as Partial)} /> + onClick={() => setFineTuning({ [axis]: val + step })} /> setFineTuning({ [axis]: val + step * 10 } as Partial)} /> + onClick={() => setFineTuning({ [axis]: val + step * 10 })} /> ); }; @@ -336,11 +311,10 @@ function TransformTab() { - - - - + + + + ); } @@ -348,7 +322,6 @@ function TransformTab() { function InspectionTab() { const checklist = useModelStore((s) => s.checklist); const setStatus = useModelStore((s) => s.setChecklistItemStatus); - return ( @@ -356,7 +329,8 @@ function InspectionTab() { {checklist.map((item, i) => { const y = 0.075 - i * 0.045; - const color = item.status === 'approved' ? '#22c55e' : item.status === 'rejected' ? '#dc2626' : '#94a3b8'; + const color = item.status === 'approved' ? '#22c55e' + : item.status === 'rejected' ? '#dc2626' : '#94a3b8'; return ( @@ -367,14 +341,10 @@ function InspectionTab() { anchorX="left" anchorY="middle" maxWidth={0.18}> {item.label} - setStatus(item.id, 'approved')} /> - setStatus(item.id, 'rejected')} /> + setStatus(item.id, 'approved')} /> + setStatus(item.id, 'rejected')} /> ); })} @@ -390,7 +360,7 @@ function ShareTab(p: XRHudInWorldProps) { anchorX="center" anchorY="middle"> {isLive ? '● TRANSMITINDO AO VIVO' : '○ Transmissão desligada'} - {isLive && ( + {isLive ? ( <> Código da sala @@ -409,8 +379,7 @@ function ShareTab(p: XRHudInWorldProps) { label="✕ Encerrar transmissão" color="#dc2626" onClick={() => p.onStopLive?.()} /> - )} - {!isLive && ( + ) : ( <>