Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-14 18:05:01 +00:00
parent 4a8fd8b005
commit eb7b7f53d1
+63 -94
View File
@@ -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<THREE.Group, { open: boolean; onToggle: () => void }>(
function WristToggle({ open, onToggle }, ref) {
return (
<group ref={ref}>
<mesh position={[0, 0, -0.0005]}>
<planeGeometry args={[0.064, 0.032]} />
<meshBasicMaterial color="#3b82f6" transparent opacity={0.6} />
</mesh>
<mesh onClick={(e) => { e.stopPropagation(); onToggle(); }}>
<planeGeometry args={[0.06, 0.028]} />
<meshBasicMaterial color={open ? '#3b82f6' : '#1a1a1a'} transparent opacity={0.95} />
</mesh>
<Text position={[0, 0, 0.001]} fontSize={0.0085} color="#ffffff" anchorX="center" anchorY="middle">
{open ? '✕ Fechar' : '☰ Menu'}
</Text>
</group>
);
},
);
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<THREE.Group>(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 (
<>
<WristToggleReal ref={wristRef} open={open} onToggle={() => setOpen((v) => !v)} />
<WristToggle ref={wristRef} open={open} onToggle={() => setOpen((v) => !v)} />
{open && (
<group ref={panelRef}>
<FloatingPanel tab={tab} setTab={setTab} {...props} />
@@ -96,43 +109,17 @@ export function XRHudInWorld(props: XRHudInWorldProps) {
);
}
const WristToggleReal = forwardRef<THREE.Group, { open: boolean; onToggle: () => void }>(
function WristToggleReal({ open, onToggle }, ref) {
return (
<group ref={ref}>
<mesh
onClick={(e) => { e.stopPropagation(); onToggle(); }}
onPointerOver={(e) => e.stopPropagation()}
>
<planeGeometry args={[0.06, 0.028]} />
<meshBasicMaterial color={open ? '#3b82f6' : '#1a1a1a'} transparent opacity={0.95} />
</mesh>
<mesh position={[0, 0, -0.0005]}>
<planeGeometry args={[0.064, 0.032]} />
<meshBasicMaterial color="#3b82f6" transparent opacity={0.6} />
</mesh>
<Text position={[0, 0, 0.001]} fontSize={0.0085} color="#ffffff" anchorX="center" anchorY="middle">
{open ? '✕ Fechar' : '☰ Menu'}
</Text>
</group>
);
}
);
// ─────────────────────────────────────────────────────────────────────────────
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 (
<XR3DPanel size={[W, H]}>
{/* Title */}
<Text position={[-W / 2 + 0.012, H / 2 - 0.018, 0.002]} fontSize={0.011} color="#3b82f6"
anchorX="left" anchorY="middle">
TrackSteelXR · HUD AR
@@ -142,23 +129,15 @@ function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => vo
Botão A (esq) abre/fecha
</Text>
{/* Tabs row */}
<group position={[0, H / 2 - 0.045, 0.001]}>
{tabs.map((t, i) => (
<XR3DButton
key={t.id}
position={[-W / 2 + 0.045 + i * 0.092, 0, 0]}
size={[0.085, 0.022]}
label={t.label}
icon={t.icon}
active={tab === t.id}
onClick={() => setTab(t.id)}
fontSize={0.0075}
/>
<XR3DButton key={t.id}
position={[-W / 2 + 0.045 + i * 0.092, 0, 0]} size={[0.085, 0.022]}
label={t.label} icon={t.icon} active={tab === t.id}
onClick={() => setTab(t.id)} fontSize={0.0075} />
))}
</group>
{/* Content area */}
<group position={[0, -0.012, 0.001]}>
{tab === 'scene' && <SceneTab />}
{tab === 'tools' && <ToolsTab {...p} />}
@@ -181,8 +160,8 @@ function SceneTab() {
if (models.length === 0) {
return (
<Text fontSize={0.01} color="#94a3b8" anchorX="center" anchorY="middle">
Nenhuma peça na cena. Saia do AR para carregar arquivos.
<Text fontSize={0.011} color="#94a3b8" anchorX="center" anchorY="middle" maxWidth={0.45}>
Nenhuma peça na cena. Saia do AR para carregar arquivos no Viewer.
</Text>
);
}
@@ -190,19 +169,17 @@ function SceneTab() {
return (
<group>
<Text position={[-0.24, 0.11, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
Toque para selecionar peça ativa
Toque no nome para tornar a peça ativa
</Text>
{models.slice(0, 5).map((m, i) => {
const y = 0.085 - i * 0.04;
const isActive = m.id === activeId;
return (
<group key={m.id} position={[0, y, 0]}>
{/* Color dot */}
<mesh position={[-0.235, 0, 0.001]}>
<circleGeometry args={[0.005, 16]} />
<meshBasicMaterial color={m.color} />
</mesh>
{/* Name (tappable to make active) */}
<group onClick={(e) => { e.stopPropagation(); setActive(m.id); }}>
<mesh position={[-0.11, 0, 0]}>
<planeGeometry args={[0.22, 0.026]} />
@@ -214,7 +191,6 @@ function SceneTab() {
{m.fileName}
</Text>
</group>
{/* Action buttons */}
<XR3DButton position={[0.04, 0, 0]} size={[0.04, 0.024]}
label={m.visible ? '👁' : '⊘'} fontSize={0.012}
onClick={() => toggleVisible(m.id)} />
@@ -248,29 +224,27 @@ function ToolsTab(p: XRHudInWorldProps) {
return (
<group>
{/* Render mode */}
<Text position={[-0.24, 0.11, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">Render</Text>
<XR3DButton position={[-0.16, 0.085, 0]} size={[0.075, 0.022]} label="Sólido"
active={renderMode === 'solid'} onClick={() => setRenderMode('solid')} />
<XR3DButton position={[-0.08, 0.085, 0]} size={[0.075, 0.022]} label="Wire"
active={renderMode === 'wireframe'} onClick={() => setRenderMode('wireframe')} />
<XR3DButton position={[0.0, 0.085, 0]} size={[0.075, 0.022]} label="Bordas"
<XR3DButton position={[0.0, 0.085, 0]} size={[0.075, 0.022]} label="Bordas"
active={renderMode === 'edges'} onClick={() => setRenderMode('edges')} />
{/* Toggles */}
<Text position={[-0.24, 0.05, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">Toggles</Text>
<XR3DButton position={[-0.16, 0.025, 0]} size={[0.075, 0.022]} label={`Grid ${showGrid ? 'ON' : 'OFF'}`}
active={showGrid} onClick={() => setShowGrid(!showGrid)} />
<XR3DButton position={[-0.08, 0.025, 0]} size={[0.075, 0.022]} label={`Medir ${measureMode ? 'ON' : 'OFF'}`}
active={measureMode} onClick={() => setMeasureMode(!measureMode)} />
<XR3DButton position={[0.0, 0.025, 0]} size={[0.075, 0.022]} label={`Snap ${p.snapToPlanes ? 'ON' : 'OFF'}`}
<XR3DButton position={[0.0, 0.025, 0]} size={[0.075, 0.022]} label={`Snap ${p.snapToPlanes ? 'ON' : 'OFF'}`}
active={p.snapToPlanes} onClick={p.onToggleSnap} />
<XR3DButton position={[0.08, 0.025, 0]} size={[0.075, 0.022]} label={`Escala ${p.allowScale ? 'ON' : 'OFF'}`}
active={p.allowScale} onClick={p.onToggleAllowScale} />
<XR3DButton position={[0.16, 0.025, 0]} size={[0.075, 0.022]} label={p.placementMode ? 'Posic…' : 'Reposic.'}
<XR3DButton position={[0.16, 0.025, 0]} size={[0.075, 0.022]}
label={p.placementMode ? 'Posic…' : 'Reposic.'}
active={p.placementMode} onClick={p.onTogglePlacement} />
{/* Opacity slider buttons */}
<Text position={[-0.24, -0.02, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
Opacidade {Math.round(opacity * 100)}%
</Text>
@@ -285,12 +259,11 @@ function ToolsTab(p: XRHudInWorldProps) {
<XR3DButton position={[0.06, -0.045, 0]} size={[0.05, 0.022]} label="100%"
onClick={() => setOpacity(1)} />
{/* Scale presets */}
<Text position={[-0.24, -0.085, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
Escala {scaleRatio.label}
</Text>
{SCALE_LABELS.map((lbl, i) => {
const preset = SCALE_PRESETS.find(p => p.label === lbl)!;
const preset = SCALE_PRESETS.find((sp) => sp.label === lbl)!;
return (
<XR3DButton key={lbl}
position={[-0.16 + i * 0.06, -0.11, 0]} size={[0.055, 0.022]} label={lbl}
@@ -307,7 +280,9 @@ function TransformTab() {
const setFineTuning = useModelStore((s) => 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}
</Text>
<XR3DButton position={[-0.10, 0, 0]} size={[0.04, 0.024]} label="−−"
onClick={() => setFineTuning({ [axis]: val - step * 10 } as Partial<typeof fineTuning>)} />
onClick={() => setFineTuning({ [axis]: val - step * 10 })} />
<XR3DButton position={[-0.05, 0, 0]} size={[0.04, 0.024]} label=""
onClick={() => setFineTuning({ [axis]: val - step } as Partial<typeof fineTuning>)} />
onClick={() => setFineTuning({ [axis]: val - step })} />
<Text position={[0.0, 0, 0]} fontSize={0.009} color="#3b82f6" anchorX="center" anchorY="middle">
{display}
</Text>
<XR3DButton position={[0.05, 0, 0]} size={[0.04, 0.024]} label="+"
onClick={() => setFineTuning({ [axis]: val + step } as Partial<typeof fineTuning>)} />
onClick={() => setFineTuning({ [axis]: val + step })} />
<XR3DButton position={[0.10, 0, 0]} size={[0.04, 0.024]} label="++"
onClick={() => setFineTuning({ [axis]: val + step * 10 } as Partial<typeof fineTuning>)} />
onClick={() => setFineTuning({ [axis]: val + step * 10 })} />
</group>
);
};
@@ -336,11 +311,10 @@ function TransformTab() {
<Row label="Pos X" axis="posX" isRot={false} y={0.105} />
<Row label="Pos Y" axis="posY" isRot={false} y={0.075} />
<Row label="Pos Z" axis="posZ" isRot={false} y={0.045} />
<Row label="Rot X" axis="rotX" isRot={true} y={0.005} />
<Row label="Rot Y" axis="rotY" isRot={true} y={-0.025} />
<Row label="Rot Z" axis="rotZ" isRot={true} y={-0.055} />
<XR3DButton position={[0, -0.105, 0]} size={[0.12, 0.026]} label="↺ Reset"
onClick={reset} />
<Row label="Rot X" axis="rotX" isRot={true} y={0.005} />
<Row label="Rot Y" axis="rotY" isRot={true} y={-0.025} />
<Row label="Rot Z" axis="rotZ" isRot={true} y={-0.055} />
<XR3DButton position={[0, -0.105, 0]} size={[0.12, 0.026]} label="↺ Reset" onClick={reset} />
</group>
);
}
@@ -348,7 +322,6 @@ function TransformTab() {
function InspectionTab() {
const checklist = useModelStore((s) => s.checklist);
const setStatus = useModelStore((s) => s.setChecklistItemStatus);
return (
<group>
<Text position={[-0.24, 0.11, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
@@ -356,7 +329,8 @@ function InspectionTab() {
</Text>
{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 (
<group key={item.id} position={[0, y, 0]}>
<mesh position={[-0.235, 0, 0]}>
@@ -367,14 +341,10 @@ function InspectionTab() {
anchorX="left" anchorY="middle" maxWidth={0.18}>
{item.label}
</Text>
<XR3DButton position={[0.06, 0, 0]} size={[0.05, 0.024]}
label="✓ OK" color="#16a34a"
active={item.status === 'approved'}
onClick={() => setStatus(item.id, 'approved')} />
<XR3DButton position={[0.115, 0, 0]} size={[0.05, 0.024]}
label="✗ NOK" color="#dc2626"
active={item.status === 'rejected'}
onClick={() => setStatus(item.id, 'rejected')} />
<XR3DButton position={[0.06, 0, 0]} size={[0.05, 0.024]} label="✓ OK" color="#16a34a"
active={item.status === 'approved'} onClick={() => setStatus(item.id, 'approved')} />
<XR3DButton position={[0.115, 0, 0]} size={[0.05, 0.024]} label="✗ NOK" color="#dc2626"
active={item.status === 'rejected'} onClick={() => setStatus(item.id, 'rejected')} />
</group>
);
})}
@@ -390,7 +360,7 @@ function ShareTab(p: XRHudInWorldProps) {
anchorX="center" anchorY="middle">
{isLive ? '● TRANSMITINDO AO VIVO' : '○ Transmissão desligada'}
</Text>
{isLive && (
{isLive ? (
<>
<Text position={[0, 0.07, 0]} fontSize={0.010} color="#94a3b8" anchorX="center" anchorY="middle">
Código da sala
@@ -409,8 +379,7 @@ function ShareTab(p: XRHudInWorldProps) {
label="✕ Encerrar transmissão" color="#dc2626"
onClick={() => p.onStopLive?.()} />
</>
)}
{!isLive && (
) : (
<>
<Text position={[0, 0.04, 0]} fontSize={0.010} color="#cbd5e1" anchorX="center" anchorY="middle"
maxWidth={0.45}>