diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index d4d7276..7b5edce 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -7,7 +7,7 @@ import { XR3DButton, XR3DPanel } from './XRPanel3D'; import { useModelStore, SCALE_PRESETS } from '@/stores/useModelStore'; import { getBroadcastSource } from '@/lib/webrtc/broadcastSource'; -type Tab = 'scene' | 'tools' | 'transform' | 'inspection' | 'share'; +type Tab = 'scene' | 'tools' | 'inspection' | 'share' | 'webxr'; interface XRHudInWorldProps { freeMove: boolean; @@ -204,9 +204,9 @@ function FloatingPanel({ 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: 'webxr', label: 'WebXR', icon: '⚙' }, ]; const W = 0.5, H = 0.36; return ( @@ -243,9 +243,9 @@ function FloatingPanel({ {tab === 'scene' && } {tab === 'tools' && } - {tab === 'transform' && } {tab === 'inspection' && } {tab === 'share' && } + {tab === 'webxr' && } ); @@ -533,3 +533,46 @@ function ShareTab(p: XRHudInWorldProps) { ); } + +function WebXRTab() { + const xrFeatures = useModelStore((s) => s.xrFeatures); + const setXRFeature = useModelStore((s) => s.setXRFeature); + const reset = useModelStore((s) => s.resetXRFeatures); + const keys: { k: keyof typeof xrFeatures; label: string }[] = [ + { 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 ( + + + Desligue uma feature por vez para descobrir qual ativa o grid de segurança do Quest. + + + Mudanças entram em vigor ao sair e re-entrar no AR. + + {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 ( + setXRFeature(it.k, !on)} + fontSize={0.0085} /> + ); + })} + + + ); +} diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 85615f6..9804999 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -29,19 +29,36 @@ if (navigator.xr) { }); } +// Lê flags de feature WebXR do localStorage (default: todas ON). +// O usuário liga/desliga pelo HUD AR (aba WebXR) para diagnosticar qual +// feature ativa o grid de segurança do Quest. Mudanças exigem sair e re-entrar do AR. +function loadXRFeatures() { + const defaults = { + handTracking: true, planeDetection: true, hitTest: true, domOverlay: true, + anchors: true, meshDetection: true, depthSensing: true, layers: true, + bodyTracking: true, lightEstimation: true, + }; + try { + const raw = localStorage.getItem('xrFeatures'); + if (raw) return { ...defaults, ...JSON.parse(raw) }; + } catch {} + return defaults; +} +const _xrf = loadXRFeatures(); +console.log('[XR] Features:', _xrf); + const store = createXRStore({ hand: { left: true, right: true }, controller: { left: true, right: true }, - // Audit de features WebXR — pedimos só o que o app usa, para reduzir - // gatilhos do Space Sense / Guardian do Quest (grid vermelho/azul perto - // de paredes e objetos). Mantemos hit-test, plane-detection e hand-tracking; - // desligamos anchors, mesh-detection, depth-sensing e layers que não usamos. - anchors: false, - meshDetection: false, - depthSensing: false, - layers: false, - bodyTracking: false, - // handTracking, planeDetection, hitTest, domOverlay ficam no default (true) + handTracking: _xrf.handTracking, + planeDetection: _xrf.planeDetection, + hitTest: _xrf.hitTest, + domOverlay: _xrf.domOverlay, + anchors: _xrf.anchors, + meshDetection: _xrf.meshDetection, + depthSensing: _xrf.depthSensing, + layers: _xrf.layers, + bodyTracking: _xrf.bodyTracking, }); // ─── XRModel ─────────────────────────────────────────── diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index d0f3a36..16bc7e9 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -1,5 +1,53 @@ import { create } from 'zustand'; +// ── Persistência de placement (fineTuning + escala) e flags WebXR ───── +const PLACEMENT_KEY = 'tsxr_placements_v1'; +const XRFEAT_KEY = 'xrFeatures'; + +export interface XRFeatureFlags { + handTracking: boolean; + planeDetection: boolean; + hitTest: boolean; + domOverlay: boolean; + anchors: boolean; + meshDetection: boolean; + depthSensing: boolean; + layers: boolean; + bodyTracking: boolean; +} +const DEFAULT_XR_FEATURES: XRFeatureFlags = { + handTracking: true, planeDetection: true, hitTest: true, domOverlay: true, + anchors: true, meshDetection: true, depthSensing: true, layers: true, + bodyTracking: true, +}; +function loadXRFeatures(): XRFeatureFlags { + try { + const raw = localStorage.getItem(XRFEAT_KEY); + if (raw) return { ...DEFAULT_XR_FEATURES, ...JSON.parse(raw) }; + } catch {} + return { ...DEFAULT_XR_FEATURES }; +} +function saveXRFeatures(f: XRFeatureFlags) { + try { localStorage.setItem(XRFEAT_KEY, JSON.stringify(f)); } catch {} +} + +interface StoredPlacement { fineTuning: FineTuning; } +function loadPlacements(): Record { + try { + const raw = localStorage.getItem(PLACEMENT_KEY); + if (raw) return JSON.parse(raw); + } catch {} + return {}; +} +function savePlacement(fileName: string, fineTuning: FineTuning) { + try { + const all = loadPlacements(); + all[fileName] = { fineTuning }; + localStorage.setItem(PLACEMENT_KEY, JSON.stringify(all)); + } catch {} +} + + export interface ScaleRatio { label: string; // e.g. "1:50", "2:1", "1:1" factor: number; // render multiplier: 1:50 => 0.02, 2:1 => 2, 1:1 => 1 @@ -181,6 +229,11 @@ interface ModelStore { addScreenshot: (dataUrl: string) => void; removeScreenshot: (index: number) => void; clearScreenshots: () => void; + + // ── WebXR feature toggles (diagnóstico do grid de segurança Quest) ──── + xrFeatures: XRFeatureFlags; + setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void; + resetXRFeatures: () => void; } /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ @@ -205,8 +258,12 @@ export const useModelStore = create((set, get) => ({ } const id = `mdl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; const color = MODEL_COLORS[state.models.length % MODEL_COLORS.length]; - // Stagger position so multiple models don't perfectly overlap const offsetIdx = state.models.length; + // Recupera placement anterior salvo (mesmo fileName) — sobrevive a sair/entrar do AR. + const saved = loadPlacements()[m.fileName]; + const fineTuning: FineTuning = saved?.fineTuning + ? { ...saved.fineTuning } + : { ...defaultFineTuning, posX: offsetIdx * 0.15 }; const newModel: SceneModel = { id, fileName: m.fileName, @@ -215,7 +272,7 @@ export const useModelStore = create((set, get) => ({ color, visible: true, locked: false, - fineTuning: { ...defaultFineTuning, posX: offsetIdx * 0.15 }, + fineTuning, }; const models = [...state.models, newModel]; set({ models, activeModelId: id, ...syncActive({ models, activeModelId: id }) }); @@ -287,17 +344,23 @@ export const useModelStore = create((set, get) => ({ fineTuning: { ...defaultFineTuning }, setFineTuning: (ft) => set((state) => { const newFT = { ...state.fineTuning, ...ft }; - // Also write to the active model's fineTuning - const models = state.activeModelId - ? state.models.map(m => m.id === state.activeModelId ? { ...m, fineTuning: newFT } : m) + const activeId = state.activeModelId; + const models = activeId + ? state.models.map(m => m.id === activeId ? { ...m, fineTuning: newFT } : m) : state.models; + // Persiste placement por fileName para sobreviver à saída/re-entrada do AR. + const active = models.find(m => m.id === activeId); + if (active) savePlacement(active.fileName, newFT); return { fineTuning: newFT, models }; }), resetFineTuning: () => set((state) => { const reset = { ...defaultFineTuning }; - const models = state.activeModelId - ? state.models.map(m => m.id === state.activeModelId ? { ...m, fineTuning: reset } : m) + const activeId = state.activeModelId; + const models = activeId + ? state.models.map(m => m.id === activeId ? { ...m, fineTuning: reset } : m) : state.models; + const active = models.find(m => m.id === activeId); + if (active) savePlacement(active.fileName, reset); return { fineTuning: reset, models }; }), @@ -401,4 +464,16 @@ export const useModelStore = create((set, get) => ({ screenshots: state.screenshots.filter((_, i) => i !== index), })), clearScreenshots: () => set({ screenshots: [] }), + + xrFeatures: loadXRFeatures(), + setXRFeature: (key, value) => set((state) => { + const next = { ...state.xrFeatures, [key]: value }; + saveXRFeatures(next); + return { xrFeatures: next }; + }), + resetXRFeatures: () => { + const next = { ...DEFAULT_XR_FEATURES }; + saveXRFeatures(next); + set({ xrFeatures: next }); + }, }));