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:
@@ -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
@@ -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 ───────────────────────────────────────────
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user