Criou WebXR tab + persistência

X-Lovable-Edit-ID: edt-bd177570-37c0-4c8f-85f5-142f96fcf1aa
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-18 21:24:50 +00:00
3 changed files with 155 additions and 20 deletions
+46 -3
View File
@@ -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({
<group position={[0, -0.012, 0.001]}>
{tab === 'scene' && <SceneTab />}
{tab === 'tools' && <ToolsTab {...p} />}
{tab === 'transform' && <TransformTab />}
{tab === 'inspection' && <InspectionTab />}
{tab === 'share' && <ShareTab {...p} />}
{tab === 'webxr' && <WebXRTab />}
</group>
</XR3DPanel>
);
@@ -533,3 +533,46 @@ function ShareTab(p: XRHudInWorldProps) {
</group>
);
}
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 (
<group>
<Text position={[-0.24, 0.115, 0]} fontSize={0.0085} color="#94a3b8" anchorX="left" maxWidth={0.48}>
Desligue uma feature por vez para descobrir qual ativa o grid de segurança do Quest.
</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.
</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>
);
}
+27 -10
View File
@@ -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 ───────────────────────────────────────────
+82 -7
View File
@@ -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<string, StoredPlacement> {
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<ModelStore>((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<ModelStore>((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<ModelStore>((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<ModelStore>((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 });
},
}));