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 });
+ },
}));