🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 14:10:16

This commit is contained in:
2026-05-24 14:10:17 +00:00
parent 678d04cc95
commit febb533f5e
4 changed files with 253 additions and 12 deletions
+72 -1
View File
@@ -1,4 +1,4 @@
import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download } from 'lucide-react'; import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download, Footprints, Compass, Map } from 'lucide-react';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
@@ -44,6 +44,9 @@ export function ViewerControls() {
selectionMode, setSelectionMode, selectionMode, setSelectionMode,
selectedElementKeys, hiddenElementKeys, isolatedElementKeys, selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection, hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection,
desktopCameraMode, setDesktopCameraMode,
xrEnvironment, setXrEnvironment,
xrScaleMode, setXrScaleMode,
} = useModelStore(); } = useModelStore();
const activeModel = models.find(m => m.id === activeModelId); const activeModel = models.find(m => m.id === activeModelId);
const hasSelection = selectedElementKeys.size > 0; const hasSelection = selectedElementKeys.size > 0;
@@ -290,6 +293,74 @@ export function ViewerControls() {
)} )}
</div> </div>
{/* Seletor de Câmera (Orbit vs Walk) */}
<Button
variant={desktopCameraMode === 'walk' ? 'default' : 'outline'}
size="sm"
className="gap-2 h-9"
onClick={() => {
const next = desktopCameraMode === 'orbit' ? 'walk' : 'orbit';
setDesktopCameraMode(next);
if (next === 'walk') {
toast.info("Passeio 3D Ativo: clique no visualizador para prender o mouse. WASD = andar, Q/E = subir/descer, ESC = soltar mouse.");
}
}}
title="Alternar entre câmera orbital e passeio em primeira pessoa"
>
{desktopCameraMode === 'walk' ? <Footprints className="h-3.5 w-3.5" /> : <Compass className="h-3.5 w-3.5" />}
<span className="font-mono text-xs">
{desktopCameraMode === 'walk' ? 'Passeio 3D' : 'Câmera'}
</span>
</Button>
{/* Seletor de Cenário 2D */}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 h-9" title="Alterar cenário de fundo do visualizador">
<Map className="h-3.5 w-3.5" />
<span className="font-mono text-xs">
{xrEnvironment === 'passthrough' ? 'Estúdio' : xrEnvironment === 'office' ? 'Escritório' : 'Campo Aberto'}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-56 space-y-2" side="top" align="start">
<div className="flex flex-col gap-1.5">
<span className="font-mono text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-1">
Cenário de Fundo
</span>
<Button
variant={xrEnvironment === 'passthrough' ? 'default' : 'ghost'}
size="sm"
className="justify-start font-mono text-xs h-8"
onClick={() => setXrEnvironment('passthrough')}
>
🌌 Estúdio (Padrão)
</Button>
<Button
variant={xrEnvironment === 'office' ? 'default' : 'ghost'}
size="sm"
className="justify-start font-mono text-xs h-8"
onClick={() => {
setXrEnvironment('office');
setXrScaleMode('tabletop');
}}
>
🏢 Escritório (VR Mesa)
</Button>
<Button
variant={xrEnvironment === 'field' ? 'default' : 'ghost'}
size="sm"
className="justify-start font-mono text-xs h-8"
onClick={() => {
setXrEnvironment('field');
setXrScaleMode('realscale');
}}
>
🌳 Campo Aberto (VR Grama)
</Button>
</div>
</PopoverContent>
</Popover>
{/* Screenshot */} {/* Screenshot */}
<Button <Button
@@ -0,0 +1,154 @@
import { useEffect, useRef } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { PointerLockControls } from '@react-three/drei';
import * as THREE from 'three';
import { useModelStore } from '@/stores/useModelStore';
export function DesktopFirstPersonControls() {
const { camera } = useThree();
const activeMode = useModelStore((s) => s.desktopCameraMode);
const xrEnvironment = useModelStore((s) => s.xrEnvironment);
// Armazena o estado atual das teclas de locomoção
const keys = useRef({
w: false,
a: false,
s: false,
d: false,
arrowup: false,
arrowleft: false,
arrowdown: false,
arrowright: false,
q: false, // voar para baixo
e: false, // voar para cima
});
useEffect(() => {
if (activeMode !== 'walk') return;
const handleKeyDown = (e: KeyboardEvent) => {
const k = e.key.toLowerCase();
if (k in keys.current) {
keys.current[k as keyof typeof keys.current] = true;
}
};
const handleKeyUp = (e: KeyboardEvent) => {
const k = e.key.toLowerCase();
if (k in keys.current) {
keys.current[k as keyof typeof keys.current] = false;
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [activeMode]);
// Define posições iniciais amigáveis ao alternar para o modo passeio
const lastMode = useRef<string | null>(null);
useEffect(() => {
if (activeMode === 'walk' && lastMode.current !== 'walk') {
if (xrEnvironment === 'office') {
// Posiciona o operador em pé na ponta da mesa
camera.position.set(0, 1.4, 1.3);
camera.lookAt(0, 0.85, 0);
} else {
// Posiciona no campo aberto em escala real
camera.position.set(0, 1.65, 3);
camera.lookAt(0, 1.0, 0);
}
}
lastMode.current = activeMode;
}, [activeMode, xrEnvironment, camera]);
useFrame((state, delta) => {
if (activeMode !== 'walk') return;
const speed = 2.0 * delta; // Metros por frame adaptativo
const rotSpeed = 1.5 * delta;
const direction = new THREE.Vector3();
const side = new THREE.Vector3();
// Obtém vetor de olhar da câmera
camera.getWorldDirection(direction);
direction.y = 0; // Trava o vetor na horizontal para não voar para cima/baixo ao olhar
direction.normalize();
// Obtém vetor lateral
side.crossVectors(camera.up, direction).normalize();
// 1. Leituras do teclado
let moveFwd = 0;
let moveSide = 0;
let moveUp = 0;
if (keys.current.w || keys.current.arrowup) moveFwd += 1;
if (keys.current.s || keys.current.arrowdown) moveFwd -= 1;
if (keys.current.d || keys.current.arrowright) moveSide -= 1;
if (keys.current.a || keys.current.arrowleft) moveSide += 1;
if (keys.current.e) moveUp += 1;
if (keys.current.q) moveUp -= 1;
camera.position.addScaledVector(direction, moveFwd * speed);
camera.position.addScaledVector(side, moveSide * speed);
camera.position.y += moveUp * speed;
// 2. Leituras da Gamepad API (Xbox / PlayStation)
const gamepads = navigator.getGamepads();
const gp = gamepads[0] || gamepads[1];
if (gp) {
const deadzone = 0.15;
// Analógico Esquerdo (Locomoção)
const stickLX = gp.axes[0]; // Eixo lateral
const stickLY = gp.axes[1]; // Eixo frente/trás
if (Math.abs(stickLY) > deadzone) {
camera.position.addScaledVector(direction, -stickLY * speed);
}
if (Math.abs(stickLX) > deadzone) {
camera.position.addScaledVector(side, -stickLX * speed);
}
// Analógico Direito (Rotação da Câmera)
const stickRX = gp.axes[2]; // Rotação Y
const stickRY = gp.axes[3]; // Olhar X cima/baixo
if (Math.abs(stickRX) > deadzone) {
camera.rotation.y -= stickRX * rotSpeed * 1.6;
}
if (Math.abs(stickRY) > deadzone) {
// Rotaciona verticalmente
camera.rotation.x -= stickRY * rotSpeed * 1.6;
// Limita ângulo vertical para evitar inverter a visão (entre -80° e 80°)
camera.rotation.x = Math.max(-1.4, Math.min(1.4, camera.rotation.x));
}
// Gatilhos para controle de altura (RT sobe, LT desce)
const triggerLT = gp.buttons[6];
const triggerRT = gp.buttons[7];
if (triggerRT && triggerRT.value > 0.1) {
camera.position.y += triggerRT.value * speed;
}
if (triggerLT && triggerLT.value > 0.1) {
camera.position.y -= triggerLT.value * speed;
}
}
// Trava de colisão com o chão/mesa para manter realismo
const minHeight = xrEnvironment === 'office' ? 0.9 : 0.1;
if (camera.position.y < minHeight) {
camera.position.y = minHeight;
}
});
if (activeMode !== 'walk') return null;
return <PointerLockControls makeDefault />;
}
+23 -11
View File
@@ -23,6 +23,7 @@ import {
XREnvironmentSelector, XREnvironmentSelector,
} from './XRSceneComponents'; } from './XRSceneComponents';
import { XRGrabbable } from './XRGrabbable'; import { XRGrabbable } from './XRGrabbable';
import { DesktopFirstPersonControls } from './DesktopFirstPersonControls';
import { XRHitTestPlacement } from './XRHitTestPlacement'; import { XRHitTestPlacement } from './XRHitTestPlacement';
import { XRControllerMeasure } from './XRControllerMeasure'; import { XRControllerMeasure } from './XRControllerMeasure';
import { ControllerLocomotion } from './ControllerLocomotion'; import { ControllerLocomotion } from './ControllerLocomotion';
@@ -1205,6 +1206,7 @@ function SceneInsideXR({
const activeModel = models.find(m => m.id === activeId); const activeModel = models.find(m => m.id === activeId);
const isActiveLocked = !!activeModel?.locked; const isActiveLocked = !!activeModel?.locked;
const xrEnvironment = useModelStore((s) => s.xrEnvironment); const xrEnvironment = useModelStore((s) => s.xrEnvironment);
const desktopCameraMode = useModelStore((s) => s.desktopCameraMode);
const rigRef = useRef<THREE.Group>(null); const rigRef = useRef<THREE.Group>(null);
const devkit = useDevKit(); const devkit = useDevKit();
@@ -1225,9 +1227,15 @@ function SceneInsideXR({
// Modo 2D normal (não imersivo) // Modo 2D normal (não imersivo)
return ( return (
<> <>
<ambientLight intensity={0.6} /> {xrEnvironment === 'passthrough' && (
<directionalLight position={[5, 10, 5]} intensity={1} castShadow /> <>
<directionalLight position={[-5, 5, -5]} intensity={0.3} /> <ambientLight intensity={0.6} />
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
<directionalLight position={[-5, 5, -5]} intensity={0.3} />
</>
)}
<XREnvironmentSelector />
<Suspense fallback={<LoadingFallback />}> <Suspense fallback={<LoadingFallback />}>
<SceneModels /> <SceneModels />
@@ -1246,14 +1254,18 @@ function SceneInsideXR({
<VisibilityApplier /> <VisibilityApplier />
<SceneRefCapture /> <SceneRefCapture />
<OrbitControls {desktopCameraMode === 'orbit' ? (
makeDefault <OrbitControls
enableDamping makeDefault
dampingFactor={0.1} enableDamping
minDistance={0.05} dampingFactor={0.1}
maxDistance={50} minDistance={0.05}
enabled={!positionMode} maxDistance={50}
/> enabled={!positionMode}
/>
) : (
<DesktopFirstPersonControls />
)}
</> </>
); );
} }
+4
View File
@@ -311,6 +311,8 @@ interface ModelStore {
setXrEnvironment: (env: 'passthrough' | 'office' | 'field') => void; setXrEnvironment: (env: 'passthrough' | 'office' | 'field') => void;
xrScaleMode: 'tabletop' | 'realscale'; xrScaleMode: 'tabletop' | 'realscale';
setXrScaleMode: (mode: 'tabletop' | 'realscale') => void; setXrScaleMode: (mode: 'tabletop' | 'realscale') => void;
desktopCameraMode: 'orbit' | 'walk';
setDesktopCameraMode: (mode: 'orbit' | 'walk') => void;
} }
/** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */
@@ -695,4 +697,6 @@ export const useModelStore = create<ModelStore>((set, get) => ({
setXrEnvironment: (env) => set({ xrEnvironment: env }), setXrEnvironment: (env) => set({ xrEnvironment: env }),
xrScaleMode: 'realscale', xrScaleMode: 'realscale',
setXrScaleMode: (mode) => set({ xrScaleMode: mode }), setXrScaleMode: (mode) => set({ xrScaleMode: mode }),
desktopCameraMode: 'orbit',
setDesktopCameraMode: (mode) => set({ desktopCameraMode: mode }),
})); }));