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 { useModelStore, SCALE_PRESETS } from '@/stores/useModelStore';
|
||||||
import { getBroadcastSource } from '@/lib/webrtc/broadcastSource';
|
import { getBroadcastSource } from '@/lib/webrtc/broadcastSource';
|
||||||
|
|
||||||
type Tab = 'scene' | 'tools' | 'transform' | 'inspection' | 'share';
|
type Tab = 'scene' | 'tools' | 'inspection' | 'share' | 'webxr';
|
||||||
|
|
||||||
interface XRHudInWorldProps {
|
interface XRHudInWorldProps {
|
||||||
freeMove: boolean;
|
freeMove: boolean;
|
||||||
@@ -204,9 +204,9 @@ function FloatingPanel({
|
|||||||
const tabs: { id: Tab; label: string; icon: string }[] = [
|
const tabs: { id: Tab; label: string; icon: string }[] = [
|
||||||
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
{ id: 'scene', label: 'Cena', icon: '🧩' },
|
||||||
{ id: 'tools', label: 'Ferram.', icon: '🛠' },
|
{ id: 'tools', label: 'Ferram.', icon: '🛠' },
|
||||||
{ id: 'transform', label: 'Mover', icon: '↔' },
|
|
||||||
{ id: 'inspection', label: 'Inspeção', icon: '✓' },
|
{ id: 'inspection', label: 'Inspeção', icon: '✓' },
|
||||||
{ id: 'share', label: 'Compart.', icon: '📡' },
|
{ id: 'share', label: 'Compart.', icon: '📡' },
|
||||||
|
{ id: 'webxr', label: 'WebXR', icon: '⚙' },
|
||||||
];
|
];
|
||||||
const W = 0.5, H = 0.36;
|
const W = 0.5, H = 0.36;
|
||||||
return (
|
return (
|
||||||
@@ -243,9 +243,9 @@ function FloatingPanel({
|
|||||||
<group position={[0, -0.012, 0.001]}>
|
<group position={[0, -0.012, 0.001]}>
|
||||||
{tab === 'scene' && <SceneTab />}
|
{tab === 'scene' && <SceneTab />}
|
||||||
{tab === 'tools' && <ToolsTab {...p} />}
|
{tab === 'tools' && <ToolsTab {...p} />}
|
||||||
{tab === 'transform' && <TransformTab />}
|
|
||||||
{tab === 'inspection' && <InspectionTab />}
|
{tab === 'inspection' && <InspectionTab />}
|
||||||
{tab === 'share' && <ShareTab {...p} />}
|
{tab === 'share' && <ShareTab {...p} />}
|
||||||
|
{tab === 'webxr' && <WebXRTab />}
|
||||||
</group>
|
</group>
|
||||||
</XR3DPanel>
|
</XR3DPanel>
|
||||||
);
|
);
|
||||||
@@ -533,3 +533,46 @@ function ShareTab(p: XRHudInWorldProps) {
|
|||||||
</group>
|
</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({
|
const store = createXRStore({
|
||||||
hand: { left: true, right: true },
|
hand: { left: true, right: true },
|
||||||
controller: { left: true, right: true },
|
controller: { left: true, right: true },
|
||||||
// Audit de features WebXR — pedimos só o que o app usa, para reduzir
|
handTracking: _xrf.handTracking,
|
||||||
// gatilhos do Space Sense / Guardian do Quest (grid vermelho/azul perto
|
planeDetection: _xrf.planeDetection,
|
||||||
// de paredes e objetos). Mantemos hit-test, plane-detection e hand-tracking;
|
hitTest: _xrf.hitTest,
|
||||||
// desligamos anchors, mesh-detection, depth-sensing e layers que não usamos.
|
domOverlay: _xrf.domOverlay,
|
||||||
anchors: false,
|
anchors: _xrf.anchors,
|
||||||
meshDetection: false,
|
meshDetection: _xrf.meshDetection,
|
||||||
depthSensing: false,
|
depthSensing: _xrf.depthSensing,
|
||||||
layers: false,
|
layers: _xrf.layers,
|
||||||
bodyTracking: false,
|
bodyTracking: _xrf.bodyTracking,
|
||||||
// handTracking, planeDetection, hitTest, domOverlay ficam no default (true)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── XRModel ───────────────────────────────────────────
|
// ─── XRModel ───────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,5 +1,53 @@
|
|||||||
import { create } from 'zustand';
|
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 {
|
export interface ScaleRatio {
|
||||||
label: string; // e.g. "1:50", "2:1", "1:1"
|
label: string; // e.g. "1:50", "2:1", "1:1"
|
||||||
factor: number; // render multiplier: 1:50 => 0.02, 2:1 => 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;
|
addScreenshot: (dataUrl: string) => void;
|
||||||
removeScreenshot: (index: number) => void;
|
removeScreenshot: (index: number) => void;
|
||||||
clearScreenshots: () => 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. */
|
/** 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 id = `mdl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
const color = MODEL_COLORS[state.models.length % MODEL_COLORS.length];
|
const color = MODEL_COLORS[state.models.length % MODEL_COLORS.length];
|
||||||
// Stagger position so multiple models don't perfectly overlap
|
|
||||||
const offsetIdx = state.models.length;
|
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 = {
|
const newModel: SceneModel = {
|
||||||
id,
|
id,
|
||||||
fileName: m.fileName,
|
fileName: m.fileName,
|
||||||
@@ -215,7 +272,7 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
color,
|
color,
|
||||||
visible: true,
|
visible: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
fineTuning: { ...defaultFineTuning, posX: offsetIdx * 0.15 },
|
fineTuning,
|
||||||
};
|
};
|
||||||
const models = [...state.models, newModel];
|
const models = [...state.models, newModel];
|
||||||
set({ models, activeModelId: id, ...syncActive({ models, activeModelId: id }) });
|
set({ models, activeModelId: id, ...syncActive({ models, activeModelId: id }) });
|
||||||
@@ -287,17 +344,23 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
fineTuning: { ...defaultFineTuning },
|
fineTuning: { ...defaultFineTuning },
|
||||||
setFineTuning: (ft) => set((state) => {
|
setFineTuning: (ft) => set((state) => {
|
||||||
const newFT = { ...state.fineTuning, ...ft };
|
const newFT = { ...state.fineTuning, ...ft };
|
||||||
// Also write to the active model's fineTuning
|
const activeId = state.activeModelId;
|
||||||
const models = state.activeModelId
|
const models = activeId
|
||||||
? state.models.map(m => m.id === state.activeModelId ? { ...m, fineTuning: newFT } : m)
|
? state.models.map(m => m.id === activeId ? { ...m, fineTuning: newFT } : m)
|
||||||
: state.models;
|
: 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 };
|
return { fineTuning: newFT, models };
|
||||||
}),
|
}),
|
||||||
resetFineTuning: () => set((state) => {
|
resetFineTuning: () => set((state) => {
|
||||||
const reset = { ...defaultFineTuning };
|
const reset = { ...defaultFineTuning };
|
||||||
const models = state.activeModelId
|
const activeId = state.activeModelId;
|
||||||
? state.models.map(m => m.id === state.activeModelId ? { ...m, fineTuning: reset } : m)
|
const models = activeId
|
||||||
|
? state.models.map(m => m.id === activeId ? { ...m, fineTuning: reset } : m)
|
||||||
: state.models;
|
: state.models;
|
||||||
|
const active = models.find(m => m.id === activeId);
|
||||||
|
if (active) savePlacement(active.fileName, reset);
|
||||||
return { fineTuning: reset, models };
|
return { fineTuning: reset, models };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -401,4 +464,16 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
screenshots: state.screenshots.filter((_, i) => i !== index),
|
screenshots: state.screenshots.filter((_, i) => i !== index),
|
||||||
})),
|
})),
|
||||||
clearScreenshots: () => set({ screenshots: [] }),
|
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