diff --git a/packages/app/src/components/Header.tsx b/packages/app/src/components/Header.tsx
index 9f906fe4..e82fb44d 100644
--- a/packages/app/src/components/Header.tsx
+++ b/packages/app/src/components/Header.tsx
@@ -1,6 +1,7 @@
import { lazy, Suspense, useEffect, useState } from "react";
import { BookOpen } from "@phosphor-icons/react/dist/ssr/BookOpen";
import { Mouse } from "@phosphor-icons/react/dist/ssr/Mouse";
+import { Goggles } from "@phosphor-icons/react/dist/ssr/Goggles";
import { Sparkle } from "@phosphor-icons/react/dist/ssr/Sparkle";
import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
import { Export } from "@phosphor-icons/react/dist/ssr/Export";
@@ -37,6 +38,7 @@ import { useAppCommands } from "@/hooks/useAppCommands";
import { COMMAND_ICONS } from "@/lib/command-icons";
import { useCapabilities } from "@/lib/capabilities";
import { useNativeMenu } from "@/hooks/useNativeMenu";
+import { xrStore, useXRPresenting, useXRSupportStore } from "@/stores/xr-store";
// Lazy so the prefs dialog (and its wasm-backed keybinding hook) doesn't
// bloat the Header bundle until the user opens it.
@@ -307,6 +309,36 @@ function RayTracingSubmenu() {
);
}
+/** XR entries for the View menu. Always shown — disabled when the browser
+ * doesn't report support, so users can see the capability exists. */
+function XRMenuItems() {
+ const vr = useXRSupportStore((s) => s.vr);
+ const ar = useXRSupportStore((s) => s.ar);
+ const presenting = useXRPresenting();
+
+ if (presenting) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
export function Header({ onAboutOpen, onProductOpen, onSave, onOpen, onShareOpen, onVersionHistoryOpen, children }: HeaderProps) {
const user = useAuthStore((s) => s.user);
const isAnonymous = useAuthStore((s) => s.isAnonymous);
@@ -565,6 +597,7 @@ export function Header({ onAboutOpen, onProductOpen, onSave, onOpen, onShareOpen
>
{t("menu.view.input_preferences")}
+
diff --git a/packages/app/src/components/Viewport.tsx b/packages/app/src/components/Viewport.tsx
index 016ac448..f874e06b 100644
--- a/packages/app/src/components/Viewport.tsx
+++ b/packages/app/src/components/Viewport.tsx
@@ -7,7 +7,6 @@ import { DrawingView } from "./DrawingView";
import { TriangleInspectionPanel } from "./TriangleInspector";
import { RayTracedViewportOverlay } from "./RayTracedViewport";
import { FollowModeToggle } from "./FollowModeToggle";
-import { EnterXRButton } from "./xr/EnterXRButton";
import { xrStore, useXRPresenting } from "@/stores/xr-store";
import {
useUiStore,
@@ -350,7 +349,6 @@ export function Viewport() {
drags in the gaps; the toggle itself re-enables them. */}
{!electronicsActive && (
-
)}
diff --git a/packages/app/src/components/xr/EnterXRButton.tsx b/packages/app/src/components/xr/EnterXRButton.tsx
deleted file mode 100644
index 4f750f9a..00000000
--- a/packages/app/src/components/xr/EnterXRButton.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useXR } from "@react-three/xr";
-import { xrStore, useXRSupportStore } from "@/stores/xr-store";
-
-/**
- * Floating Enter-XR control. Renders nothing if neither VR nor AR is
- * supported, so desktop users never see it.
- */
-export function EnterXRButton() {
- const checked = useXRSupportStore((s) => s.checked);
- const vr = useXRSupportStore((s) => s.vr);
- const ar = useXRSupportStore((s) => s.ar);
- // `mode` becomes a string (e.g. "immersive-vr") while a session is active.
- const mode = useXR((s) => s.mode);
-
- if (!checked || (!vr && !ar)) return null;
- const presenting = mode != null;
-
- if (presenting) {
- return (
-
- );
- }
-
- return (
-
- {ar && (
-
- )}
- {vr && (
-
- )}
-
- );
-}
diff --git a/packages/app/src/stores/xr-store.ts b/packages/app/src/stores/xr-store.ts
index 5c5c4892..6885b63e 100644
--- a/packages/app/src/stores/xr-store.ts
+++ b/packages/app/src/stores/xr-store.ts
@@ -12,6 +12,10 @@ export const xrStore = createXRStore({
hand: true,
controller: true,
foveation: 0,
+ // Disable the bundled iwer device emulator. It would otherwise auto-inject a
+ // floating "Enter XR" DOM button on localhost, which clashes with our
+ // in-menubar XR entry. Real headsets are unaffected.
+ emulate: false,
});
type SupportState = {