Fix label positioning, add billboard mode, fix XR entry shift, and fix config system
Label Positioning Fixes: - Fix labels accounting for mesh scaling using maximumWorld coordinates - Labels now properly positioned on scaled objects (spheres, boxes, etc.) - Restore world→local coordinate transformation in updateLabelPosition Billboard Mode Implementation: - Add configurable label rendering modes: fixed, billboard, dynamic, distance-based - Implement billboard mode (labels always face camera using BILLBOARDMODE_Y) - Add label rendering mode to AppConfig with default 'billboard' - Add UI selector in ConfigModal for label rendering mode - Observable pattern updates all existing labels when mode changes XR Entry Positioning Fix: - Synchronize desktop camera position to platform before entering XR - Transfer camera world position and rotation to prevent scene shift - Reset physics velocity on XR entry to prevent drift - Add debug logging for position synchronization Config System Architecture Fix: - Create singleton appConfigInstance to ensure single source of truth - Update DiagramObject to use singleton instead of creating instances - Update DiagramManager to use singleton - Fix ConfigModal to update AppConfig directly (was only updating legacy config) - ConfigModal now triggers Observable notifications via appConfigInstance setters - Maintain legacy config for backward compatibility - Fixes issue where label rendering mode changes didn't take effect Files Modified: - src/diagram/diagramObject.ts - Label positioning, billboard mode, singleton config - src/diagram/diagramManager.ts - Use singleton config - src/util/appConfig.ts - Add labelRenderingMode, export singleton - src/util/appConfigType.ts - Add LabelRenderingMode type - src/react/pages/configModal.tsx - Update AppConfig directly, add label mode UI - src/util/functions/groundMeshObserver.ts - Add camera position sync on XR entry - public/api/user/features - Update test config - package.json - Version bump 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c1503d959e
commit
970f6fc78a
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "immersive",
|
"name": "immersive",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.8-27",
|
"version": "0.0.8-28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
"privateDesigns": false,
|
"privateDesigns": false,
|
||||||
"encryptedDesigns": false,
|
"encryptedDesigns": false,
|
||||||
"editData": false,
|
"editData": false,
|
||||||
"config": false,
|
"config": true,
|
||||||
"enterImmersive": true,
|
"enterImmersive": true,
|
||||||
"launchMetaQuest": true
|
"launchMetaQuest": true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, W
|
|||||||
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import {AppConfig} from "../util/appConfig";
|
import {appConfigInstance} from "../util/appConfig";
|
||||||
import {buildEntityActionManager} from "./functions/buildEntityActionManager";
|
import {buildEntityActionManager} from "./functions/buildEntityActionManager";
|
||||||
import {DefaultScene} from "../defaultScene";
|
import {DefaultScene} from "../defaultScene";
|
||||||
import {DiagramMenuManager} from "./diagramMenuManager";
|
import {DiagramMenuManager} from "./diagramMenuManager";
|
||||||
@ -17,7 +17,6 @@ import {ControllerEvent} from "../controllers/types/controllerEvent";
|
|||||||
|
|
||||||
export class DiagramManager {
|
export class DiagramManager {
|
||||||
private readonly _logger = log.getLogger('DiagramManager');
|
private readonly _logger = log.getLogger('DiagramManager');
|
||||||
public readonly _config: AppConfig;
|
|
||||||
private readonly _controllerObservable: Observable<ControllerEvent>;
|
private readonly _controllerObservable: Observable<ControllerEvent>;
|
||||||
private readonly _diagramEntityActionManager: ActionManager;
|
private readonly _diagramEntityActionManager: ActionManager;
|
||||||
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
|
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
|
||||||
@ -40,7 +39,6 @@ export class DiagramManager {
|
|||||||
constructor(readyObservable: Observable<boolean>) {
|
constructor(readyObservable: Observable<boolean>) {
|
||||||
this._me = getMe();
|
this._me = getMe();
|
||||||
this._scene = DefaultScene.Scene;
|
this._scene = DefaultScene.Scene;
|
||||||
this._config = new AppConfig();
|
|
||||||
this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, controllerObservable, readyObservable);
|
this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, controllerObservable, readyObservable);
|
||||||
this._diagramEntityActionManager = buildEntityActionManager(controllerObservable);
|
this._diagramEntityActionManager = buildEntityActionManager(controllerObservable);
|
||||||
this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this);
|
this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this);
|
||||||
@ -133,8 +131,8 @@ export class DiagramManager {
|
|||||||
this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject);
|
this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get config(): AppConfig {
|
public get config() {
|
||||||
return this._config;
|
return appConfigInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {xyztovec} from "./functions/vectorConversion";
|
|||||||
import {AnimatedLineTexture} from "../util/animatedLineTexture";
|
import {AnimatedLineTexture} from "../util/animatedLineTexture";
|
||||||
import {getToolboxColors} from "../toolbox/toolbox";
|
import {getToolboxColors} from "../toolbox/toolbox";
|
||||||
import {findClosestColor} from "../util/functions/findClosestColor";
|
import {findClosestColor} from "../util/functions/findClosestColor";
|
||||||
|
import {appConfigInstance} from "../util/appConfig";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a Color3 to a hex color string
|
* Converts a Color3 to a hex color string
|
||||||
@ -62,6 +63,7 @@ export class DiagramObject {
|
|||||||
private _fromMesh: AbstractMesh;
|
private _fromMesh: AbstractMesh;
|
||||||
private _toMesh: AbstractMesh;
|
private _toMesh: AbstractMesh;
|
||||||
private _meshRemovedObserver: Observer<AbstractMesh>;
|
private _meshRemovedObserver: Observer<AbstractMesh>;
|
||||||
|
private _configObserver: Observer<any>;
|
||||||
// Position caching for connection optimization
|
// Position caching for connection optimization
|
||||||
private _lastFromPosition: Vector3 = null;
|
private _lastFromPosition: Vector3 = null;
|
||||||
private _lastToPosition: Vector3 = null;
|
private _lastToPosition: Vector3 = null;
|
||||||
@ -70,6 +72,12 @@ export class DiagramObject {
|
|||||||
constructor(scene: Scene, eventObservable: Observable<DiagramEvent>, options?: DiagramObjectOptionsType) {
|
constructor(scene: Scene, eventObservable: Observable<DiagramEvent>, options?: DiagramObjectOptionsType) {
|
||||||
this._eventObservable = eventObservable;
|
this._eventObservable = eventObservable;
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
|
|
||||||
|
// Subscribe to config changes to update label rendering mode
|
||||||
|
this._configObserver = appConfigInstance.onConfigChangedObservable.add(() => {
|
||||||
|
this.updateLabelRenderingMode();
|
||||||
|
});
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
this._logger.debug('DiagramObject constructor called with options', options);
|
this._logger.debug('DiagramObject constructor called with options', options);
|
||||||
if (options.diagramEntity) {
|
if (options.diagramEntity) {
|
||||||
@ -154,37 +162,69 @@ export class DiagramObject {
|
|||||||
this._labelBack.parent = this._label;
|
this._labelBack.parent = this._label;
|
||||||
this._labelBack.metadata = {exportable: true};
|
this._labelBack.metadata = {exportable: true};
|
||||||
this.updateLabelPosition();
|
this.updateLabelPosition();
|
||||||
|
this.updateLabelRenderingMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLabelRenderingMode() {
|
||||||
|
if (!this._label) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = appConfigInstance.current.labelRenderingMode || 'billboard';
|
||||||
|
|
||||||
|
// Reset billboard mode first
|
||||||
|
this._label.billboardMode = Mesh.BILLBOARDMODE_NONE;
|
||||||
|
if (this._labelBack) {
|
||||||
|
this._labelBack.billboardMode = Mesh.BILLBOARDMODE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'billboard':
|
||||||
|
// Billboard mode - labels always face camera (Y-axis only to prevent tilting)
|
||||||
|
this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
||||||
|
if (this._labelBack) {
|
||||||
|
this._labelBack.billboardMode = Mesh.BILLBOARDMODE_Y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'fixed':
|
||||||
|
// Fixed mode - no billboard (default state, already set above)
|
||||||
|
break;
|
||||||
|
case 'dynamic':
|
||||||
|
// Dynamic mode - to be implemented in future
|
||||||
|
// TODO: Implement screen-space positioning
|
||||||
|
this._logger.warn('Dynamic label rendering mode not yet implemented');
|
||||||
|
break;
|
||||||
|
case 'distance':
|
||||||
|
// Distance-based mode - to be implemented in future
|
||||||
|
// TODO: Implement distance-based offset
|
||||||
|
this._logger.warn('Distance-based label rendering mode not yet implemented');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateLabelPosition() {
|
public updateLabelPosition() {
|
||||||
if (this._label) {
|
if (this._label) {
|
||||||
this._mesh.computeWorldMatrix(true);
|
this._mesh.computeWorldMatrix(true);
|
||||||
this._mesh.refreshBoundingInfo({});
|
this._mesh.refreshBoundingInfo();
|
||||||
|
|
||||||
if (this._from && this._to) {
|
if (this._from && this._to) {
|
||||||
//this._label.position.x = .06;
|
// Connection labels (arrows/lines)
|
||||||
//this._label.position.z = .06;
|
|
||||||
this._label.position.y = .05;
|
this._label.position.y = .05;
|
||||||
this._label.rotation.y = Math.PI / 2;
|
this._label.rotation.y = Math.PI / 2;
|
||||||
this._labelBack.rotation.y = Math.PI;
|
this._labelBack.rotation.y = Math.PI;
|
||||||
this._labelBack.position.z = 0.001
|
this._labelBack.position.z = 0.001;
|
||||||
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
|
||||||
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const top =
|
// Standard object labels - convert world space to parent's local space
|
||||||
this._mesh.getBoundingInfo().boundingBox.maximumWorld;
|
// This accounts for mesh scaling, which is not included in boundingBox.maximum
|
||||||
|
const top = this._mesh.getBoundingInfo().boundingBox.maximumWorld;
|
||||||
const temp = new TransformNode("temp", this._scene);
|
const temp = new TransformNode("temp", this._scene);
|
||||||
temp.position = top;
|
temp.position = top;
|
||||||
temp.setParent(this._baseTransform);
|
temp.setParent(this._baseTransform);
|
||||||
const y = temp.position.y;
|
const y = temp.position.y;
|
||||||
temp.dispose();
|
temp.dispose();
|
||||||
this._label.position.y = y + .06;
|
this._label.position.y = y + 0.06;
|
||||||
//this._labelBack.position.y = y + .06;
|
|
||||||
this._labelBack.rotation.y = Math.PI;
|
this._labelBack.rotation.y = Math.PI;
|
||||||
this._labelBack.position.z = 0.001
|
this._labelBack.position.z = 0.001;
|
||||||
//this._label.billboardMode = Mesh.BILLBOARDMODE_Y;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,6 +360,8 @@ export class DiagramObject {
|
|||||||
this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id)
|
this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id)
|
||||||
this._scene?.onAfterRenderObservable.remove(this._sceneObserver);
|
this._scene?.onAfterRenderObservable.remove(this._sceneObserver);
|
||||||
this._sceneObserver = null;
|
this._sceneObserver = null;
|
||||||
|
appConfigInstance?.onConfigChangedObservable.remove(this._configObserver);
|
||||||
|
this._configObserver = null;
|
||||||
this._mesh?.setParent(null);
|
this._mesh?.setParent(null);
|
||||||
this._mesh?.dispose(true, false);
|
this._mesh?.dispose(true, false);
|
||||||
this._mesh = null;
|
this._mesh = null;
|
||||||
|
|||||||
@ -32,11 +32,12 @@ function createDynamicTexture(text: string, font: string, DTWidth: number, DTHei
|
|||||||
function createMaterial(dynamicTexture: DynamicTexture): Material {
|
function createMaterial(dynamicTexture: DynamicTexture): Material {
|
||||||
const mat = new StandardMaterial("text-mat", DefaultScene.Scene);
|
const mat = new StandardMaterial("text-mat", DefaultScene.Scene);
|
||||||
//mat.diffuseColor = Color3.Black();
|
//mat.diffuseColor = Color3.Black();
|
||||||
mat.disableLighting = false;
|
mat.disableLighting = true;
|
||||||
//mat.backFaceCulling = false;
|
mat.backFaceCulling = true;
|
||||||
mat.emissiveTexture = dynamicTexture;
|
mat.emissiveTexture = dynamicTexture;
|
||||||
mat.diffuseTexture = dynamicTexture;
|
mat.diffuseTexture = dynamicTexture;
|
||||||
mat.metadata = {exportable: true};
|
mat.metadata = {exportable: true};
|
||||||
|
|
||||||
//mat.freeze();
|
//mat.freeze();
|
||||||
return mat;
|
return mat;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {Group, Modal, SegmentedControl, Stack, Switch} from "@mantine/core";
|
import {Group, Modal, SegmentedControl, Stack, Switch, Select} from "@mantine/core";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {setAppConfig} from "../../util/appConfig";
|
import {appConfigInstance} from "../../util/appConfig";
|
||||||
|
import {LabelRenderingMode} from "../../util/appConfigType";
|
||||||
|
|
||||||
const locationSnaps = [
|
const locationSnaps = [
|
||||||
{value: ".01", label: '1cm'},
|
{value: ".01", label: '1cm'},
|
||||||
@ -16,6 +17,12 @@ const rotationSnaps = [
|
|||||||
{value: "180", label: '180°'},
|
{value: "180", label: '180°'},
|
||||||
{value: "360", label: '360°'},
|
{value: "360", label: '360°'},
|
||||||
]
|
]
|
||||||
|
const labelRenderingModes = [
|
||||||
|
{value: 'fixed', label: 'Fixed'},
|
||||||
|
{value: 'billboard', label: 'Billboard (Always Face Camera)'},
|
||||||
|
{value: 'dynamic', label: 'Dynamic (Coming Soon)', disabled: true},
|
||||||
|
{value: 'distance', label: 'Distance-based (Coming Soon)', disabled: true},
|
||||||
|
]
|
||||||
let defaultConfig =
|
let defaultConfig =
|
||||||
{
|
{
|
||||||
locationSnap: '.1',
|
locationSnap: '.1',
|
||||||
@ -24,7 +31,8 @@ let defaultConfig =
|
|||||||
rotationSnapEnabled: true,
|
rotationSnapEnabled: true,
|
||||||
flyModeEnabled: true,
|
flyModeEnabled: true,
|
||||||
snapTurnSnap: '45',
|
snapTurnSnap: '45',
|
||||||
snapTurnSnapEnabled: false
|
snapTurnSnapEnabled: false,
|
||||||
|
labelRenderingMode: 'billboard' as LabelRenderingMode
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const newConfig = JSON.parse(localStorage.getItem('config'));
|
const newConfig = JSON.parse(localStorage.getItem('config'));
|
||||||
@ -42,19 +50,30 @@ export default function ConfigModal({configOpened, closeConfig}) {
|
|||||||
const [rotationSnap, setRotationSnap] = useState(defaultConfig.rotationSnap);
|
const [rotationSnap, setRotationSnap] = useState(defaultConfig.rotationSnap);
|
||||||
const [rotationSnapEnabled, setRotationSnapEnabled] = useState(defaultConfig.rotationSnapEnabled);
|
const [rotationSnapEnabled, setRotationSnapEnabled] = useState(defaultConfig.rotationSnapEnabled);
|
||||||
const [flyModeEnabled, setFlyModeEnabled] = useState(defaultConfig.flyModeEnabled);
|
const [flyModeEnabled, setFlyModeEnabled] = useState(defaultConfig.flyModeEnabled);
|
||||||
|
const [labelRenderingMode, setLabelRenderingMode] = useState<LabelRenderingMode>(defaultConfig.labelRenderingMode);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const config = {
|
// Update AppConfig singleton instance directly
|
||||||
|
// This triggers Observable notifications to all DiagramObjects
|
||||||
|
appConfigInstance.setLabelRenderingMode(labelRenderingMode);
|
||||||
|
appConfigInstance.setFlyMode(flyModeEnabled);
|
||||||
|
appConfigInstance.setGridSnap(parseFloat(locationSnap));
|
||||||
|
appConfigInstance.setRotateSnap(parseFloat(rotationSnap));
|
||||||
|
appConfigInstance.setTurnSnap(parseFloat(snapTurnSnap));
|
||||||
|
|
||||||
|
// Also update legacy config for backward compatibility
|
||||||
|
const legacyConfig = {
|
||||||
locationSnap: locationSnap,
|
locationSnap: locationSnap,
|
||||||
locationSnapEnabled: locationSnapEnabled,
|
locationSnapEnabled: locationSnapEnabled,
|
||||||
rotationSnap: rotationSnap,
|
rotationSnap: rotationSnap,
|
||||||
rotationSnapEnabled: rotationSnapEnabled,
|
rotationSnapEnabled: rotationSnapEnabled,
|
||||||
snapTurnSnap: snapTurnSnap,
|
snapTurnSnap: snapTurnSnap,
|
||||||
snapTurnSnapEnabled: snapTurnSnapEnabled,
|
snapTurnSnapEnabled: snapTurnSnapEnabled,
|
||||||
flyModeEnabled: flyModeEnabled
|
flyModeEnabled: flyModeEnabled,
|
||||||
}
|
labelRenderingMode: labelRenderingMode
|
||||||
setAppConfig(config);
|
};
|
||||||
|
localStorage.setItem('config', JSON.stringify(legacyConfig));
|
||||||
|
|
||||||
}, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled]);
|
}, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled, labelRenderingMode]);
|
||||||
return (
|
return (
|
||||||
<Modal onClose={closeConfig} opened={configOpened}>
|
<Modal onClose={closeConfig} opened={configOpened}>
|
||||||
<h1>Configuration</h1>
|
<h1>Configuration</h1>
|
||||||
@ -104,6 +123,16 @@ export default function ConfigModal({configOpened, closeConfig}) {
|
|||||||
value={snapTurnSnap}
|
value={snapTurnSnap}
|
||||||
onChange={setSnapTurnSnap}/>
|
onChange={setSnapTurnSnap}/>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Group key="labelmode">
|
||||||
|
<label key="label">Label Rendering Mode</label>
|
||||||
|
<Select
|
||||||
|
w={300}
|
||||||
|
key="select"
|
||||||
|
data={labelRenderingModes}
|
||||||
|
value={labelRenderingMode}
|
||||||
|
onChange={(value) => setLabelRenderingMode(value as LabelRenderingMode)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {Observable} from "@babylonjs/core";
|
import {Observable} from "@babylonjs/core";
|
||||||
import {AppConfigType} from "./appConfigType";
|
import {AppConfigType, LabelRenderingMode} from "./appConfigType";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
export class AppConfig {
|
export class AppConfig {
|
||||||
public readonly onConfigChangedObservable = new Observable<AppConfigType>();
|
public readonly onConfigChangedObservable = new Observable<AppConfigType>();
|
||||||
@ -13,7 +13,8 @@ export class AppConfig {
|
|||||||
newRelicKey: null,
|
newRelicKey: null,
|
||||||
newRelicAccount: null,
|
newRelicAccount: null,
|
||||||
physicsEnabled: false,
|
physicsEnabled: false,
|
||||||
flyMode: true
|
flyMode: true,
|
||||||
|
labelRenderingMode: 'billboard'
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -74,12 +75,21 @@ export class AppConfig {
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setLabelRenderingMode(mode: LabelRenderingMode) {
|
||||||
|
this._currentConfig.labelRenderingMode = mode;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
private save() {
|
private save() {
|
||||||
localStorage.setItem('appConfig', JSON.stringify(this._currentConfig));
|
localStorage.setItem('appConfig', JSON.stringify(this._currentConfig));
|
||||||
this.onConfigChangedObservable.notifyObservers(this._currentConfig, -1);
|
this.onConfigChangedObservable.notifyObservers(this._currentConfig, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton instance for app-wide configuration
|
||||||
|
// Use this instead of creating new AppConfig() instances
|
||||||
|
export const appConfigInstance = new AppConfig();
|
||||||
|
|
||||||
let defaultConfig: ConfigType =
|
let defaultConfig: ConfigType =
|
||||||
{
|
{
|
||||||
locationSnap: '.1',
|
locationSnap: '.1',
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
export type LabelRenderingMode = 'fixed' | 'billboard' | 'dynamic' | 'distance';
|
||||||
|
|
||||||
export type AppConfigType = {
|
export type AppConfigType = {
|
||||||
id?: number,
|
id?: number,
|
||||||
currentDiagramId?: string,
|
currentDiagramId?: string,
|
||||||
@ -10,5 +12,6 @@ export type AppConfigType = {
|
|||||||
newRelicAccount?: string,
|
newRelicAccount?: string,
|
||||||
passphrase?: string,
|
passphrase?: string,
|
||||||
flyMode?: boolean,
|
flyMode?: boolean,
|
||||||
|
labelRenderingMode?: LabelRenderingMode,
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import {AbstractMesh, Vector3, WebXRDefaultExperience, WebXRMotionControllerManager, WebXRState} from "@babylonjs/core";
|
import {AbstractMesh, Quaternion, Vector3, WebXRDefaultExperience, WebXRMotionControllerManager, WebXRState} from "@babylonjs/core";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import {WebController} from "../../controllers/webController";
|
import {WebController} from "../../controllers/webController";
|
||||||
import {Rigplatform} from "../../controllers/rigplatform";
|
import {Rigplatform} from "../../controllers/rigplatform";
|
||||||
@ -33,6 +33,46 @@ export async function groundMeshObserver(ground: AbstractMesh,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('enterXr', async () => {
|
window.addEventListener('enterXr', async () => {
|
||||||
|
// Synchronize desktop camera position with platform before entering XR
|
||||||
|
// This prevents the jarring scene shift when transitioning to immersive mode
|
||||||
|
const scene = ground.getScene();
|
||||||
|
const desktopCamera = scene.activeCamera;
|
||||||
|
const platform = scene.getMeshByName('platform');
|
||||||
|
|
||||||
|
if (desktopCamera && platform) {
|
||||||
|
// Get desktop camera's world position
|
||||||
|
const cameraWorldPos = desktopCamera.globalPosition.clone();
|
||||||
|
|
||||||
|
// Calculate platform position accounting for camera's local offset
|
||||||
|
// Camera is at (0, 1.6, 0) in local space relative to cameraTransform
|
||||||
|
const platformTargetPos = cameraWorldPos.clone();
|
||||||
|
platformTargetPos.y = 0.01; // Platform stays at floor level
|
||||||
|
|
||||||
|
// Subtract camera's local Y offset (1.6m height) from platform position
|
||||||
|
const cameraLocalOffset = new Vector3(0, 1.59, 0); // 1.6 - 0.01
|
||||||
|
platformTargetPos.subtractInPlace(cameraLocalOffset);
|
||||||
|
|
||||||
|
// Set platform position
|
||||||
|
platform.setAbsolutePosition(platformTargetPos);
|
||||||
|
|
||||||
|
// Match platform rotation to desktop camera's viewing direction
|
||||||
|
const cameraForward = desktopCamera.getDirection(Vector3.Forward());
|
||||||
|
const yaw = Math.atan2(cameraForward.x, cameraForward.z);
|
||||||
|
platform.rotationQuaternion = Quaternion.FromEulerAngles(0, yaw, 0);
|
||||||
|
|
||||||
|
// Reset physics velocity to prevent drift on XR entry
|
||||||
|
if (platform.physicsBody) {
|
||||||
|
platform.physicsBody.setLinearVelocity(Vector3.Zero());
|
||||||
|
platform.physicsBody.setAngularVelocity(Vector3.Zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Synchronized camera position before XR entry:", {
|
||||||
|
cameraWorldPos: cameraWorldPos.asArray(),
|
||||||
|
platformPos: platformTargetPos.asArray(),
|
||||||
|
yaw: yaw
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
|
await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
|
||||||
logger.debug("Entering XR Experience");
|
logger.debug("Entering XR Experience");
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user