Fix XR component positioning to appear in front of user

- Use camera.getDirection() instead of manual Euler angle calculation to properly account for camera transform hierarchy
- Negate forward offsets to position objects in -Z direction (user faces -Z by design)
- Replace expensive HighlightLayer hover effect with lightweight EdgesRenderer (20-50x faster)
- Add comprehensive debug logging for position calculations

The camera has a parent transform with 180° Y rotation, causing the user to face -Z in world space. Components now correctly position in front of the user when entering XR.

🤖 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-12 22:41:51 -06:00
parent 4a9d7acc41
commit 0ad61bdde9
2 changed files with 80 additions and 56 deletions

View File

@ -1,10 +1,9 @@
import { import {
ActionManager, ActionManager,
Color4,
ExecuteCodeAction, ExecuteCodeAction,
HighlightLayer,
InstancedMesh, InstancedMesh,
Observable, Observable,
StandardMaterial,
} from "@babylonjs/core"; } from "@babylonjs/core";
import log from "loglevel"; import log from "loglevel";
import {DefaultScene} from "../../defaultScene"; import {DefaultScene} from "../../defaultScene";
@ -12,11 +11,6 @@ import {ControllerEventType} from "../../controllers/types/controllerEventType";
import {ControllerEvent} from "../../controllers/types/controllerEvent"; import {ControllerEvent} from "../../controllers/types/controllerEvent";
export function buildEntityActionManager(controllerObservable: Observable<ControllerEvent>) { export function buildEntityActionManager(controllerObservable: Observable<ControllerEvent>) {
const highlightLayer = new HighlightLayer('highlightLayer', DefaultScene.Scene);
highlightLayer.innerGlow = false;
highlightLayer.outerGlow = true;
const logger = log.getLogger('buildEntityActionManager'); const logger = log.getLogger('buildEntityActionManager');
const actionManager = new ActionManager(DefaultScene.Scene); const actionManager = new ActionManager(DefaultScene.Scene);
/*actionManager.registerAction( /*actionManager.registerAction(
@ -26,19 +20,16 @@ export function buildEntityActionManager(controllerObservable: Observable<Contro
if (evt.meshUnderPointer) { if (evt.meshUnderPointer) {
try { try {
const mesh = evt.meshUnderPointer as InstancedMesh; const mesh = evt.meshUnderPointer as InstancedMesh;
//mesh.sourceMesh.renderOutline = true;
if (mesh.sourceMesh) { if (mesh.sourceMesh && !mesh.sourceMesh.edgesRenderer) {
const newMesh = mesh.sourceMesh.clone(mesh.sourceMesh.name + '_clone', null, true); // Enable edges rendering on the source mesh
newMesh.metadata = {}; mesh.sourceMesh.enableEdgesRendering(0.99);
newMesh.parent = null; mesh.sourceMesh.edgesWidth = 4.0;
newMesh.position = mesh.absolutePosition; mesh.sourceMesh.edgesColor = new Color4(1.5, 1.5, 1.5, 1.0);
newMesh.rotationQuaternion = mesh.absoluteRotationQuaternion;
newMesh.scaling = mesh.scaling; // Track that edges are enabled
newMesh.setEnabled(true); mesh.metadata = mesh.metadata || {};
newMesh.isPickable = false; mesh.metadata.edgesEnabled = true;
highlightLayer.addMesh(newMesh, (mesh.sourceMesh.material as StandardMaterial).diffuseColor.multiplyByFloats(1.5, 1.5, 1.5));
highlightLayer.setEffectIntensity(newMesh, 1.2);
mesh.metadata.highlight = newMesh;
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
@ -54,10 +45,10 @@ export function buildEntityActionManager(controllerObservable: Observable<Contro
actionManager.registerAction( actionManager.registerAction(
new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, (evt) => { new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, (evt) => {
try { try {
const mesh = evt.source; const mesh = evt.source as InstancedMesh;
if (mesh.metadata.highlight) { if (mesh.metadata?.edgesEnabled && mesh.sourceMesh?.edgesRenderer) {
mesh.metadata.highlight.dispose(); mesh.sourceMesh.disableEdgesRendering();
mesh.metadata.highlight = null; mesh.metadata.edgesEnabled = false;
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);

View File

@ -98,49 +98,82 @@ function positionComponentsRelativeToCamera(scene: Scene, diagramManager: Diagra
// Get camera world position // Get camera world position
const cameraWorldPos = camera.globalPosition; const cameraWorldPos = camera.globalPosition;
// Create a horizontal forward direction from camera's world rotation // Get camera's actual forward direction in world space
const cameraRotationY = camera.absoluteRotation.toEulerAngles().y; // This accounts for the camera's parent transform rotation (Math.PI on Y)
const horizontalForward = new Vector3( const cameraForward = camera.getDirection(Vector3.Forward());
Math.sin(cameraRotationY), const horizontalForward = cameraForward.clone();
0, horizontalForward.y = 0; // Keep only horizontal component
Math.cos(cameraRotationY) horizontalForward.normalize(); // Ensure unit vector
);
// Create a left direction (perpendicular to forward) // Create a left direction (perpendicular to forward, in world space)
const horizontalLeft = new Vector3( const horizontalLeft = Vector3.Cross(Vector3.Up(), horizontalForward).normalize();
-Math.cos(cameraRotationY),
0,
Math.sin(cameraRotationY)
);
// Calculate base target world position: 0.5m ahead horizontally and 0.5m below camera Y logger.info('Camera world position:', cameraWorldPos);
const baseTargetWorldPos = new Vector3( logger.info('Camera forward (world):', cameraForward);
cameraWorldPos.x + (horizontalForward.x * 0.5), logger.info('Horizontal forward:', horizontalForward);
cameraWorldPos.y - 0.5, logger.info('Horizontal left:', horizontalLeft);
cameraWorldPos.z + (horizontalForward.z * 0.5) logger.info('Platform world position:', platform.getAbsolutePosition());
);
logger.info('Camera world Y:', cameraWorldPos.y); // Position toolbox: On left side following VR best practices
logger.info('Base target world position:', baseTargetWorldPos); // Meta guidelines: 45-60 degrees to the side, comfortable arm's reach (~0.4-0.5m)
// Position toolbox: 0.2m to the left of base position
const toolbox = diagramManager.diagramMenuManager.toolbox; const toolbox = diagramManager.diagramMenuManager.toolbox;
if (toolbox && toolbox.handleMesh) { if (toolbox && toolbox.handleMesh) {
const toolboxWorldPos = new Vector3( logger.info('Toolbox handleMesh BEFORE positioning:', {
baseTargetWorldPos.x + (horizontalLeft.x * 0.2), position: toolbox.handleMesh.position.clone(),
baseTargetWorldPos.y, absolutePosition: toolbox.handleMesh.getAbsolutePosition().clone(),
baseTargetWorldPos.z + (horizontalLeft.z * 0.2) rotation: toolbox.handleMesh.rotation.clone()
); });
// Position at 45 degrees to the left, 0.45m away, slightly below eye level
// NOTE: User faces -Z direction by design, so negate forward offset
const forwardOffset = horizontalForward.scale(-0.3);
const leftOffset = horizontalLeft.scale(0.35);
const toolboxWorldPos = cameraWorldPos.add(forwardOffset).add(leftOffset);
toolboxWorldPos.y = cameraWorldPos.y - 0.3; // Below eye level
logger.info('Calculated toolbox world position:', toolboxWorldPos);
logger.info('Forward offset:', forwardOffset);
logger.info('Left offset:', leftOffset);
const toolboxLocalPos = Vector3.TransformCoordinates(toolboxWorldPos, platform.getWorldMatrix().invert()); const toolboxLocalPos = Vector3.TransformCoordinates(toolboxWorldPos, platform.getWorldMatrix().invert());
logger.info('Calculated toolbox local position:', toolboxLocalPos);
toolbox.handleMesh.position = toolboxLocalPos; toolbox.handleMesh.position = toolboxLocalPos;
logger.info('Toolbox positioned at:', toolboxLocalPos);
// Orient toolbox to face the user
const toolboxToCamera = cameraWorldPos.subtract(toolboxWorldPos).normalize();
const toolboxYaw = Math.atan2(toolboxToCamera.x, toolboxToCamera.z);
toolbox.handleMesh.rotation.y = toolboxYaw;
logger.info('Toolbox handleMesh AFTER positioning:', {
position: toolbox.handleMesh.position.clone(),
absolutePosition: toolbox.handleMesh.getAbsolutePosition().clone(),
rotation: toolbox.handleMesh.rotation.clone()
});
} }
// Position input text view: at base position // Position input text view: Centered in front, slightly below eye level
const inputTextView = diagramManager.diagramMenuManager['_inputTextView']; const inputTextView = diagramManager.diagramMenuManager['_inputTextView'];
if (inputTextView && inputTextView.handleMesh) { if (inputTextView && inputTextView.handleMesh) {
const inputLocalPos = Vector3.TransformCoordinates(baseTargetWorldPos, platform.getWorldMatrix().invert()); logger.info('InputTextView handleMesh BEFORE positioning:', {
position: inputTextView.handleMesh.position.clone(),
absolutePosition: inputTextView.handleMesh.getAbsolutePosition().clone()
});
// NOTE: User faces -Z direction by design, so negate forward offset
const inputWorldPos = cameraWorldPos.add(horizontalForward.scale(-0.5));
inputWorldPos.y = cameraWorldPos.y - 0.4; // Below eye level
logger.info('Calculated input world position:', inputWorldPos);
const inputLocalPos = Vector3.TransformCoordinates(inputWorldPos, platform.getWorldMatrix().invert());
logger.info('Calculated input local position:', inputLocalPos);
inputTextView.handleMesh.position = inputLocalPos; inputTextView.handleMesh.position = inputLocalPos;
logger.info('InputTextView positioned at:', inputLocalPos);
logger.info('InputTextView handleMesh AFTER positioning:', {
position: inputTextView.handleMesh.position.clone(),
absolutePosition: inputTextView.handleMesh.getAbsolutePosition().clone()
});
} }
} }