Skip to content

Fix XrMenu rendering outside of XR sessions - #9155

Merged
willeastcott merged 1 commit into
mainfrom
fix-xr-menu-visible-outside-xr
Aug 6, 2026
Merged

Fix XrMenu rendering outside of XR sessions#9155
willeastcott merged 1 commit into
mainfrom
fix-xr-menu-visible-outside-xr

Conversation

@willeastcott

Copy link
Copy Markdown
Contributor

Problem

Any app that mounts the XrMenu script renders the menu in the scene while not in XR — opaque, at the world origin. Reproduces in this repo's own xr/xr-menu example and in every web-components example that uses the script; it's only conspicuous where the camera happens to frame the origin closely (e.g. the shoe configurator, where the 7.5 cm menu fills a fifth of the frame).

Inspecting the live scene outside XR, before this change:

value
XrMenuContainer.enabled true
button element.opacity 0.85
button text element.opacity 1
_menuVisible / _currentOpacity / _targetOpacity false / 0 / 0

The internal state says "hidden" while the scene graph says "visible".

Cause

initialize hides the menu via _setMenuVisible(false), but that method opens with a no-change guard:

if (this._menuVisible === visible) return;

_menuVisible is already false from its field initializer, so the initial hide is a no-op:

  • _menuContainer keeps Entity's default enabled = true.
  • _updateMenuOpacity never runs — it's only called from update when _currentOpacity !== _targetOpacity, and both are 0 — so the elements stay at the opacities _createButton assigned.
  • Nothing corrects it later, because update returns on its first line while !app.xr.active.

_onXrEnd fails the same way from the other direction: it only requests the fade-out by setting _targetOpacity = 0, and update stops advancing that fade the moment the session ends. Exit XR with the menu open and it stays frozen in the world at its last headset-relative pose.

Both cases are one flaw: hiding is delegated to a fade loop that only runs during an XR session, while the initial hide is delegated to a setter that can't fire.

Fix

Add _hideImmediate — zero the opacities, clear hover/press state, disable the container — and call it from initialize and from _onXrEnd (after _setMenuVisible(false), which is kept so 'xr:menu:active' still fires for listeners when the menu was open).

Testing

Verified in the shoe-configurator web-components example with the patched script dropped in:

state container image opacity text opacity
pre-XR false 0 0
_setMenuVisible(true) + fade to 1 true 0.85 1
after _onXrEnd false 0 0

The show path is unaffected: _setMenuVisible(true) re-enables the container and the existing _targetOpacity > 0 guard in update still prevents the first-frame dt ≈ 0 self-disable. Lint passes.

🤖 Generated with Claude Code

`initialize` hid the menu with `_setMenuVisible(false)`, but that early-outs on
its no-change guard because `_menuVisible` is already `false`. The initial hide
was therefore a no-op: the container kept `Entity`'s default `enabled = true`,
`_updateMenuOpacity` never ran so the elements stayed at the opacities
`_createButton` assigned, and `update` bails while `xr.active` is false — so the
menu rendered opaque at the world origin for the whole pre-XR lifetime.

`_onXrEnd` had the same problem from the other direction: it only requested the
fade-out, which `update` can no longer advance once the session has ended, so a
menu that was open at exit stayed frozen in the world.

Add `_hideImmediate` to apply the hide synchronously and use it from both paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@willeastcott
willeastcott merged commit 9b78cb7 into main Aug 6, 2026
2 checks passed
@willeastcott
willeastcott deleted the fix-xr-menu-visible-outside-xr branch August 6, 2026 22:50
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Build size report

This PR does not change the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2357.8 KB — 605.3 KB — 470.0 KB —
playcanvas.min.mjs 2355.2 KB — 604.3 KB — 469.6 KB —

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant