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:
parent
4a9d7acc41
commit
0ad61bdde9
@ -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);
|
||||||
|
|||||||
@ -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()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user