Move exit XR button to toolbox class

Refactored exit XR button creation from rigplatform to toolbox for better organization and UI cohesion.

- Add setXR() methods to DiagramManager, DiagramMenuManager, and Toolbox to pass WebXRDefaultExperience after initialization
- Create setupXRButton() in Toolbox class that creates button when entering XR
- Position button at bottom-right of toolbox (x: 0.5, y: -0.35, z: 0)
- Use Y-axis rotation (Math.PI) for correct orientation within toolbox coordinate system
- Scale button to 0.2 for appropriate size
- Remove button creation code from rigplatform

Exit button now moves with toolbox and is logically grouped with other UI elements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-13 06:57:35 -06:00
parent d59c7b6e6e
commit 100c5e612c
5 changed files with 54 additions and 23 deletions

View File

@ -8,7 +8,6 @@ import {DefaultScene} from "../defaultScene";
import {ControllerEvent} from "./types/controllerEvent"; import {ControllerEvent} from "./types/controllerEvent";
import {ControllerEventType} from "./types/controllerEventType"; import {ControllerEventType} from "./types/controllerEventType";
import {controllerObservable} from "./controllers"; import {controllerObservable} from "./controllers";
import {Button} from "../objects/Button";
const RIGHT = "right"; const RIGHT = "right";
const LEFT = "left"; const LEFT = "left";
@ -45,25 +44,7 @@ export class Rigplatform {
this._xr = xr; this._xr = xr;
this.rigMesh = buildRig(xr); this.rigMesh = buildRig(xr);
this._xr.baseExperience.onStateChangedObservable.add((state) => { // Exit XR button is now created in toolbox class
if (state == 2) {
const button = Button.CreateButton("exitXr", "exitXr", this._scene, {});
button.transform.position.z = 1;
button.transform.rotation.y = Math.PI;
button.transform.position.y = 1.2;
button.transform.scaling = new Vector3(.1, .1, .1);
button.transform.parent = this.rigMesh;
button.onPointerObservable.add((evt) => {
console.log(evt);
console.log(evt.sourceEvent.type);
if (evt.sourceEvent.type == 'pointerdown') {
xr.baseExperience.exitXRAsync();
}
});
}
});
this._fixRotation(); this._fixRotation();
this._initializeControllers(); this._initializeControllers();

View File

@ -1,4 +1,4 @@
import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene} from "@babylonjs/core"; import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, WebXRDefaultExperience} from "@babylonjs/core";
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import log from "loglevel"; import log from "loglevel";
@ -29,6 +29,14 @@ export class DiagramManager {
private _moving: number = 10; private _moving: number = 10;
private _i: number = 0; private _i: number = 0;
public get diagramMenuManager(): DiagramMenuManager {
return this._diagramMenuManager;
}
public setXR(xr: WebXRDefaultExperience): void {
this._diagramMenuManager.setXR(xr);
}
constructor(readyObservable: Observable<boolean>) { constructor(readyObservable: Observable<boolean>) {
this._me = getMe(); this._me = getMe();
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;

View File

@ -1,5 +1,5 @@
import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRInputSource} from "@babylonjs/core"; import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
import {InputTextView} from "../information/inputTextView"; import {InputTextView} from "../information/inputTextView";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import log from "loglevel"; import log from "loglevel";
@ -127,4 +127,8 @@ export class DiagramMenuManager {
private notifyAll(event: DiagramEvent) { private notifyAll(event: DiagramEvent) {
this._notifier.notifyObservers(event, DiagramEventObserverMask.ALL); this._notifier.notifyObservers(event, DiagramEventObserverMask.ALL);
} }
public setXR(xr: WebXRDefaultExperience): void {
this.toolbox.setXR(xr);
}
} }

View File

@ -1,8 +1,9 @@
import {AbstractMesh, Color3, InstancedMesh, Node, Observable, Scene, TransformNode, Vector3} from "@babylonjs/core"; import {AbstractMesh, Color3, InstancedMesh, Node, Observable, Scene, TransformNode, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
import {buildColor} from "./functions/buildColor"; import {buildColor} from "./functions/buildColor";
import log from "loglevel"; import log from "loglevel";
import {Handle} from "../objects/handle"; import {Handle} from "../objects/handle";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {Button} from "../objects/Button";
const colors: string[] = [ const colors: string[] = [
"#222222", "#8b4513", "#006400", "#778899", "#222222", "#8b4513", "#006400", "#778899",
@ -18,6 +19,7 @@ export class Toolbox {
private readonly _logger = log.getLogger('Toolbox'); private readonly _logger = log.getLogger('Toolbox');
private readonly _handle: Handle; private readonly _handle: Handle;
private readonly _scene: Scene; private readonly _scene: Scene;
private _xr?: WebXRDefaultExperience;
constructor(readyObservable: Observable<boolean>) { constructor(readyObservable: Observable<boolean>) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
@ -31,6 +33,11 @@ export class Toolbox {
}); });
Toolbox._instance = this; Toolbox._instance = this;
} }
public setXR(xr: WebXRDefaultExperience): void {
this._xr = xr;
this.setupXRButton();
}
private index = 0; private index = 0;
private colorPicker: TransformNode; private colorPicker: TransformNode;
private changing = false; private changing = false;
@ -128,5 +135,33 @@ export class Toolbox {
} }
} }
private setupXRButton() {
if (!this._xr) {
this._logger.warn('XR not available, exit XR button will not be created');
return;
}
this._xr.baseExperience.onStateChangedObservable.add((state) => {
if (state == 2) { // WebXRState.IN_XR
const button = Button.CreateButton("exitXr", "exitXr", this._scene, {});
// Position button at bottom-right of toolbox, matching handle size and orientation
button.transform.position.x = 0.5; // Right side
button.transform.position.y = -0.35; // Below color grid
button.transform.position.z = 0; // Coplanar with toolbox
button.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly
button.transform.scaling = new Vector3(.2, .2, .2); // Match handle height
button.transform.parent = this._toolboxBaseNode;
button.onPointerObservable.add((evt) => {
this._logger.debug(evt);
if (evt.sourceEvent.type == 'pointerdown') {
this._xr.baseExperience.exitXRAsync();
}
});
}
});
}
} }

View File

@ -79,6 +79,9 @@ export async function groundMeshObserver(ground: AbstractMesh,
rig.turnSnap = parseFloat(config.snapTurnSnap); rig.turnSnap = parseFloat(config.snapTurnSnap);
const webController = new WebController(ground.getScene(), rig, diagramManager); const webController = new WebController(ground.getScene(), rig, diagramManager);
// Set XR on diagram manager so toolbox can create exit button
diagramManager.setXR(xr);
} }
function positionComponentsRelativeToCamera(scene: Scene, diagramManager: DiagramManager) { function positionComponentsRelativeToCamera(scene: Scene, diagramManager: DiagramManager) {