🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 13:56:02

This commit is contained in:
2026-05-24 13:56:02 +00:00
parent fd9082510f
commit 84b8a452a1
4 changed files with 175 additions and 48 deletions
+11 -3
View File
@@ -20,6 +20,7 @@ import {
XRSnapHandler, XRSnapHandler,
XRGrid, XRGrid,
XRGridAutoFollower, XRGridAutoFollower,
XREnvironmentSelector,
} from './XRSceneComponents'; } from './XRSceneComponents';
import { XRGrabbable } from './XRGrabbable'; import { XRGrabbable } from './XRGrabbable';
import { XRHitTestPlacement } from './XRHitTestPlacement'; import { XRHitTestPlacement } from './XRHitTestPlacement';
@@ -1203,6 +1204,7 @@ function SceneInsideXR({
const models = useModelStore((s) => s.models); const models = useModelStore((s) => s.models);
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 rigRef = useRef<THREE.Group>(null); const rigRef = useRef<THREE.Group>(null);
const devkit = useDevKit(); const devkit = useDevKit();
@@ -1259,9 +1261,15 @@ function SceneInsideXR({
// Modo AR imersivo (ou SimXR) // Modo AR imersivo (ou SimXR)
return ( return (
<> <>
<ambientLight intensity={0.8} /> {xrEnvironment === 'passthrough' && (
<directionalLight position={[3, 5, 3]} intensity={1.2} /> <>
<directionalLight position={[-3, 3, -3]} intensity={0.4} /> <ambientLight intensity={0.8} />
<directionalLight position={[3, 5, 3]} intensity={1.2} />
<directionalLight position={[-3, 3, -3]} intensity={0.4} />
</>
)}
<XREnvironmentSelector />
<group ref={rigRef}> <group ref={rigRef}>
<XROrigin /> <XROrigin />
+72 -38
View File
@@ -11,7 +11,7 @@ import {
stopRecording, captureScreenshot, formatElapsed, stopRecording, captureScreenshot, formatElapsed,
} from '@/hooks/useCanvasRecorder'; } from '@/hooks/useCanvasRecorder';
type Tab = 'scene' | 'tools' | 'inspection' | 'share' | 'capture' | 'webxr'; type Tab = 'scene' | 'tools' | 'inspection' | 'share' | 'capture' | 'env';
interface XRHudInWorldProps { interface XRHudInWorldProps {
freeMove: boolean; freeMove: boolean;
@@ -221,7 +221,7 @@ function FloatingPanel({
{ id: 'inspection', label: 'Inspeção', icon: '✓' }, { id: 'inspection', label: 'Inspeção', icon: '✓' },
{ id: 'capture', label: 'Captura', icon: '🎥' }, { id: 'capture', label: 'Captura', icon: '🎥' },
{ id: 'share', label: 'Compart.', icon: '📡' }, { id: 'share', label: 'Compart.', icon: '📡' },
{ id: 'webxr', label: 'WebXR', icon: '' }, { id: 'env', label: 'Ambiente', icon: '🏝' },
]; ];
const W = 0.5, H = 0.36; const W = 0.5, H = 0.36;
return ( return (
@@ -261,7 +261,7 @@ function FloatingPanel({
{tab === 'inspection' && <InspectionTab />} {tab === 'inspection' && <InspectionTab />}
{tab === 'capture' && <CaptureTab />} {tab === 'capture' && <CaptureTab />}
{tab === 'share' && <ShareTab {...p} />} {tab === 'share' && <ShareTab {...p} />}
{tab === 'webxr' && <WebXRTab />} {tab === 'env' && <EnvTab />}
</group> </group>
</XR3DPanel> </XR3DPanel>
); );
@@ -583,45 +583,79 @@ function ShareTab(p: XRHudInWorldProps) {
); );
} }
function WebXRTab() { function EnvTab() {
const xrFeatures = useModelStore((s) => s.xrFeatures); const xrEnvironment = useModelStore((s) => s.xrEnvironment);
const setXRFeature = useModelStore((s) => s.setXRFeature); const setXrEnvironment = useModelStore((s) => s.setXrEnvironment);
const reset = useModelStore((s) => s.resetXRFeatures); const xrScaleMode = useModelStore((s) => s.xrScaleMode);
const keys: { k: keyof typeof xrFeatures; label: string }[] = [ const setXrScaleMode = useModelStore((s) => s.setXrScaleMode);
{ k: 'handTracking', label: 'Hand' },
{ k: 'planeDetection', label: 'Planes' },
{ k: 'hitTest', label: 'HitTest' },
{ k: 'domOverlay', label: 'DOM Ovl' },
{ k: 'anchors', label: 'Anchors' },
{ k: 'meshDetection', label: 'Mesh' },
{ k: 'depthSensing', label: 'Depth' },
{ k: 'layers', label: 'Layers' },
{ k: 'bodyTracking', label: 'Body' },
];
return ( return (
<group> <group>
<Text position={[-0.24, 0.115, 0]} fontSize={0.0085} color="#94a3b8" anchorX="left" maxWidth={0.48}> <Text position={[-0.24, 0.11, 0]} fontSize={0.0085} color="#94a3b8" anchorX="left">
Desligue uma feature por vez para descobrir qual ativa o grid de segurança do Quest. Cenário Imersivo (VR / AR)
</Text> </Text>
<Text position={[-0.24, 0.095, 0]} fontSize={0.0075} color="#f59e0b" anchorX="left" maxWidth={0.48}>
Mudanças entram em vigor ao sair e re-entrar no AR. <XR3DButton
position={[-0.14, 0.075, 0]}
size={[0.13, 0.026]}
label="Passthrough (AR)"
active={xrEnvironment === 'passthrough'}
onClick={() => setXrEnvironment('passthrough')}
fontSize={0.008}
/>
<XR3DButton
position={[0.0, 0.075, 0]}
size={[0.13, 0.026]}
label="Escritório (VR)"
active={xrEnvironment === 'office'}
onClick={() => {
setXrEnvironment('office');
setXrScaleMode('tabletop');
}}
fontSize={0.008}
/>
<XR3DButton
position={[0.14, 0.075, 0]}
size={[0.13, 0.026]}
label="Campo Aberto (VR)"
active={xrEnvironment === 'field'}
onClick={() => {
setXrEnvironment('field');
setXrScaleMode('realscale');
}}
fontSize={0.008}
/>
<Text position={[-0.24, 0.02, 0]} fontSize={0.0085} color="#94a3b8" anchorX="left">
Escala de Apresentação do IFC
</Text>
<XR3DButton
position={[-0.08, -0.015, 0]}
size={[0.18, 0.026]}
label="Escala Real 1:1 (Caminhar)"
active={xrScaleMode === 'realscale'}
onClick={() => setXrScaleMode('realscale')}
fontSize={0.0085}
/>
<XR3DButton
position={[0.11, -0.015, 0]}
size={[0.18, 0.026]}
label="Modo Maquete (Mesa)"
active={xrScaleMode === 'tabletop'}
onClick={() => setXrScaleMode('tabletop')}
fontSize={0.0085}
/>
<Text position={[-0.24, -0.065, 0]} fontSize={0.007} color="#94a3b8" anchorX="left" maxWidth={0.48}>
Passthrough: veja o mundo real à sua volta (Realidade Aumentada).
</Text>
<Text position={[-0.24, -0.085, 0]} fontSize={0.007} color="#94a3b8" anchorX="left" maxWidth={0.48}>
Escritório: maquete interativa reduzida em cima de uma mesa virtual de reunião.
</Text>
<Text position={[-0.24, -0.105, 0]} fontSize={0.007} color="#94a3b8" anchorX="left" maxWidth={0.48}>
Campo Aberto: terreno gramado em escala real para explorar estruturas e galpões.
</Text> </Text>
{keys.map((it, i) => {
const col = i % 3;
const row = Math.floor(i / 3);
const x = -0.18 + col * 0.14;
const y = 0.05 - row * 0.045;
const on = xrFeatures[it.k];
return (
<XR3DButton key={it.k}
position={[x, y, 0]} size={[0.125, 0.034]}
label={`${it.label} ${on ? 'ON' : 'OFF'}`}
active={on} onClick={() => setXRFeature(it.k, !on)}
fontSize={0.0085} />
);
})}
<XR3DButton position={[0, -0.14, 0]} size={[0.18, 0.028]}
label="↺ Resetar (todas ON)" onClick={reset} fontSize={0.009} />
</group> </group>
); );
} }
+81 -7
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useMemo, useState, useCallback } from 'react'; import { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import { useFrame, useThree } from '@react-three/fiber'; import { useFrame, useThree } from '@react-three/fiber';
import { Grid, Billboard } from '@react-three/drei'; import { Grid, Billboard, Sky } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import * as THREE from 'three'; import * as THREE from 'three';
import { useModelStore, type SceneModel } from '@/stores/useModelStore'; import { useModelStore, type SceneModel } from '@/stores/useModelStore';
@@ -141,20 +141,39 @@ export function XRModel({ sceneModel }: { sceneModel: SceneModel }) {
const s = fineTuning.scale ?? 1; const s = fineTuning.scale ?? 1;
const scaleRatio = useModelStore((st) => st.scaleRatio); const scaleRatio = useModelStore((st) => st.scaleRatio);
const renderFactor = scaleRatio?.factor ?? 1; const renderFactor = scaleRatio?.factor ?? 1;
const xrScaleMode = useModelStore((st) => st.xrScaleMode);
const isTabletop = xrScaleMode === 'tabletop';
if (!sceneModel.visible) return null; if (!sceneModel.visible) return null;
if (!scene) return null; if (!scene) return null;
// Escala final: se for tabletop, escala de maquete proporcional ao tamanho do objeto
const maxDim = Math.max(modelInfo.size.x, modelInfo.size.y, modelInfo.size.z);
const finalScaleFactor = isTabletop
? (maxDim > 0 ? 0.5 / maxDim : 0.05)
: renderFactor;
// Posição local: no tabletop, eleva-se o modelo para que sua base coincida com Y=0 da origem do pai (topo da mesa)
const localPos: [number, number, number] = isTabletop
? [
-modelInfo.center.x + fineTuning.posX,
(-modelInfo.center.y + (modelInfo.size.y / 2)) + fineTuning.posY,
-modelInfo.center.z + fineTuning.posZ,
]
: [
-modelInfo.center.x + fineTuning.posX,
-modelInfo.center.y + fineTuning.posY,
-modelInfo.center.z + fineTuning.posZ,
];
const parentPos: [number, number, number] = isTabletop ? [0, 0.85, 0] : [0, 0, 0];
return ( return (
<group scale={[renderFactor, renderFactor, renderFactor]}> <group scale={[finalScaleFactor, finalScaleFactor, finalScaleFactor]} position={parentPos}>
<group <group
ref={ref} ref={ref}
userData={{ modelId: sceneModel.id }} userData={{ modelId: sceneModel.id }}
position={[ position={localPos}
-modelInfo.center.x + fineTuning.posX,
-modelInfo.center.y + fineTuning.posY,
-modelInfo.center.z + fineTuning.posZ,
]}
rotation={[rotXRad, rotYRad, rotZRad]} rotation={[rotXRad, rotYRad, rotZRad]}
scale={[s, s, s]} scale={[s, s, s]}
> >
@@ -541,3 +560,58 @@ export function XRGridAutoFollower() {
}, [models, scene]); }, [models, scene]);
return null; return null;
} }
export function XREnvironmentSelector() {
const xrEnvironment = useModelStore((s) => s.xrEnvironment);
if (xrEnvironment === 'passthrough') return null;
return (
<group>
{/* Luzes adicionais para ambientes imersivos virtuais */}
<ambientLight intensity={0.5} />
<directionalLight position={[5, 10, 5]} intensity={1.0} castShadow />
{/* Cenário Escritório */}
{xrEnvironment === 'office' && (
<group>
{/* Carpete do escritório */}
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[50, 50]} />
<meshStandardMaterial color="#263238" roughness={0.95} />
</mesh>
{/* Tampo da Mesa */}
<mesh position={[0, 0.825, 0]} receiveShadow castShadow>
<cylinderGeometry args={[0.7, 0.7, 0.05, 32]} />
<meshStandardMaterial color="#3e2723" roughness={0.6} metalness={0.1} />
</mesh>
{/* Perna da Mesa */}
<mesh position={[0, 0.4, 0]} castShadow>
<cylinderGeometry args={[0.08, 0.08, 0.8, 16]} />
<meshStandardMaterial color="#333333" roughness={0.4} metalness={0.8} />
</mesh>
{/* Base da perna */}
<mesh position={[0, 0.01, 0]} castShadow>
<cylinderGeometry args={[0.3, 0.3, 0.02, 24]} />
<meshStandardMaterial color="#222222" roughness={0.3} metalness={0.8} />
</mesh>
</group>
)}
{/* Cenário Campo Aberto */}
{xrEnvironment === 'field' && (
<group>
<Sky distance={450000} sunPosition={[10, 5, 10]} inclination={0} azimuth={0.25} />
{/* Chão de grama */}
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[1000, 1000]} />
<meshStandardMaterial color="#1b5e20" roughness={0.9} />
</mesh>
</group>
)}
</group>
);
}
+11
View File
@@ -305,6 +305,12 @@ interface ModelStore {
setXrHudOpen: (open: boolean) => void; setXrHudOpen: (open: boolean) => void;
xrHudTab: string; xrHudTab: string;
setXrHudTab: (tab: string) => void; setXrHudTab: (tab: string) => void;
// ── WebXR Cenários Imersivos e Escalonamento ────
xrEnvironment: 'passthrough' | 'office' | 'field';
setXrEnvironment: (env: 'passthrough' | 'office' | 'field') => void;
xrScaleMode: 'tabletop' | 'realscale';
setXrScaleMode: (mode: 'tabletop' | 'realscale') => void;
} }
/** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */
@@ -684,4 +690,9 @@ export const useModelStore = create<ModelStore>((set, get) => ({
setXrHudOpen: (open) => set({ xrHudOpen: open }), setXrHudOpen: (open) => set({ xrHudOpen: open }),
xrHudTab: 'scene', xrHudTab: 'scene',
setXrHudTab: (tab) => set({ xrHudTab: tab }), setXrHudTab: (tab) => set({ xrHudTab: tab }),
xrEnvironment: 'passthrough',
setXrEnvironment: (env) => set({ xrEnvironment: env }),
xrScaleMode: 'realscale',
setXrScaleMode: (mode) => set({ xrScaleMode: mode }),
})); }));