🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 13:56:02
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user