Migrate from legacy config to new AppConfig singleton system

Remove dual config system and migrate all code to use appConfigInstance.

**Phase 1: Update VR Controller Code**
- snapAll.ts: Replace getAppConfig() with appConfigInstance.current
  - Use `rotateSnap > 0` instead of rotationSnapEnabled flag
  - Use `locationSnap > 0` instead of locationSnapEnabled flag
  - Remove parseFloat() calls (values already numbers)
- groundMeshObserver.ts: Direct property replacement
  - flyModeEnabled → flyMode
  - snapTurnSnap → turnSnap (remove parseFloat)
- customPhysics.ts: Add enabled checks and update
  - Add `> 0` checks (was applying unconditionally)
  - Use locationSnap and rotateSnap directly

**Phase 2: Remove Legacy Config Bridge**
- vrConfigPanel.ts: Remove syncLegacyConfig() method and all calls
- configModal.tsx: Remove legacy localStorage 'config' writes

**Phase 3: Cleanup**
- appConfig.ts: Remove legacy code (ConfigType, getAppConfig(), setAppConfig())
- Remove unused log import

**Benefits:**
- Eliminates dual config system confusion
- Fixes precision error from string "0" values
- Single source of truth via appConfigInstance
- Reactive updates via Observable pattern
- Cleaner, simpler codebase

🤖 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-18 13:20:18 -06:00
parent 3cf3d996dc
commit c66da87401
7 changed files with 36 additions and 107 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "immersive", "name": "immersive",
"private": true, "private": true,
"version": "0.0.8-28", "version": "0.0.8-30",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@ -1,23 +1,23 @@
import {TransformNode, Vector3} from "@babylonjs/core"; import {TransformNode, Vector3} from "@babylonjs/core";
import {getAppConfig} from "../../util/appConfig"; import {appConfigInstance} from "../../util/appConfig";
import {snapRotateVal} from "../../util/functions/snapRotateVal"; import {snapRotateVal} from "../../util/functions/snapRotateVal";
import {snapGridVal} from "../../util/functions/snapGridVal"; import {snapGridVal} from "../../util/functions/snapGridVal";
export function snapAll(node: TransformNode, pickPoint: Vector3) { export function snapAll(node: TransformNode, pickPoint: Vector3) {
const config = getAppConfig(); const config = appConfigInstance.current;
const transform = new TransformNode('temp', node.getScene()); const transform = new TransformNode('temp', node.getScene());
transform.position = pickPoint; transform.position = pickPoint;
node.setParent(transform); node.setParent(transform);
if (config.rotationSnapEnabled) { if (config.rotateSnap > 0) {
node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), parseFloat(config.rotationSnap)); node.rotation = snapRotateVal(node.absoluteRotationQuaternion.toEulerAngles(), config.rotateSnap);
} }
if (config.locationSnapEnabled) { if (config.locationSnap > 0) {
transform.position = snapGridVal(transform.absolutePosition, parseFloat(config.locationSnap)); transform.position = snapGridVal(transform.absolutePosition, config.locationSnap);
} }
node.setParent(null); node.setParent(null);
if (config.locationSnapEnabled) { if (config.locationSnap > 0) {
node.position = snapGridVal(node.absolutePosition, parseFloat(config.locationSnap)); node.position = snapGridVal(node.absolutePosition, config.locationSnap);
} }
transform.dispose(); transform.dispose();

View File

@ -335,12 +335,10 @@ export class VRConfigPanel {
const lastValue = appConfigInstance.current.locationSnap || 0.1; const lastValue = appConfigInstance.current.locationSnap || 0.1;
appConfigInstance.setGridSnap(lastValue > 0 ? lastValue : 0.1); appConfigInstance.setGridSnap(lastValue > 0 ? lastValue : 0.1);
this.updateLocationSnapButtonStates(lastValue); this.updateLocationSnapButtonStates(lastValue);
this.syncLegacyConfig();
} else { } else {
// Disable by setting to 0 // Disable by setting to 0
appConfigInstance.setGridSnap(0); appConfigInstance.setGridSnap(0);
this.updateLocationSnapButtonStates(0); this.updateLocationSnapButtonStates(0);
this.syncLegacyConfig();
} }
}); });
@ -388,7 +386,6 @@ export class VRConfigPanel {
if (this._locationSnapEnabled) { if (this._locationSnapEnabled) {
appConfigInstance.setGridSnap(snap.value); appConfigInstance.setGridSnap(snap.value);
this.updateLocationSnapButtonStates(snap.value); this.updateLocationSnapButtonStates(snap.value);
this.syncLegacyConfig();
} }
}); });
@ -457,12 +454,10 @@ export class VRConfigPanel {
const lastValue = appConfigInstance.current.rotateSnap || 90; const lastValue = appConfigInstance.current.rotateSnap || 90;
appConfigInstance.setRotateSnap(lastValue > 0 ? lastValue : 90); appConfigInstance.setRotateSnap(lastValue > 0 ? lastValue : 90);
this.updateRotationSnapButtonStates(lastValue); this.updateRotationSnapButtonStates(lastValue);
this.syncLegacyConfig();
} else { } else {
// Disable by setting to 0 // Disable by setting to 0
appConfigInstance.setRotateSnap(0); appConfigInstance.setRotateSnap(0);
this.updateRotationSnapButtonStates(0); this.updateRotationSnapButtonStates(0);
this.syncLegacyConfig();
} }
}); });
@ -510,7 +505,6 @@ export class VRConfigPanel {
if (this._rotationSnapEnabled) { if (this._rotationSnapEnabled) {
appConfigInstance.setRotateSnap(snap.value); appConfigInstance.setRotateSnap(snap.value);
this.updateRotationSnapButtonStates(snap.value); this.updateRotationSnapButtonStates(snap.value);
this.syncLegacyConfig();
} }
}); });
@ -586,26 +580,4 @@ export class VRConfigPanel {
// - Snap turn UI update // - Snap turn UI update
// - Label rendering mode UI update // - Label rendering mode UI update
} }
/**
* Sync changes to legacy config for backward compatibility
* Legacy config is used by snapAll.ts and other older code
*/
private syncLegacyConfig(): void {
const config = appConfigInstance.current;
const legacyConfig = {
locationSnap: config.locationSnap.toString(),
locationSnapEnabled: config.locationSnap > 0,
rotationSnap: config.rotateSnap.toString(),
rotationSnapEnabled: config.rotateSnap > 0,
snapTurnSnap: config.turnSnap.toString(),
snapTurnSnapEnabled: config.turnSnap > 0,
flyModeEnabled: config.flyMode,
labelRenderingMode: config.labelRenderingMode
};
localStorage.setItem('config', JSON.stringify(legacyConfig));
this._logger.debug('Synced legacy config', legacyConfig);
}
} }

View File

@ -59,20 +59,6 @@ export default function ConfigModal({configOpened, closeConfig}) {
appConfigInstance.setGridSnap(parseFloat(locationSnap)); appConfigInstance.setGridSnap(parseFloat(locationSnap));
appConfigInstance.setRotateSnap(parseFloat(rotationSnap)); appConfigInstance.setRotateSnap(parseFloat(rotationSnap));
appConfigInstance.setTurnSnap(parseFloat(snapTurnSnap)); appConfigInstance.setTurnSnap(parseFloat(snapTurnSnap));
// Also update legacy config for backward compatibility
const legacyConfig = {
locationSnap: locationSnap,
locationSnapEnabled: locationSnapEnabled,
rotationSnap: rotationSnap,
rotationSnapEnabled: rotationSnapEnabled,
snapTurnSnap: snapTurnSnap,
snapTurnSnapEnabled: snapTurnSnapEnabled,
flyModeEnabled: flyModeEnabled,
labelRenderingMode: labelRenderingMode
};
localStorage.setItem('config', JSON.stringify(legacyConfig));
}, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled, labelRenderingMode]); }, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled, labelRenderingMode]);
return ( return (
<Modal onClose={closeConfig} opened={configOpened}> <Modal onClose={closeConfig} opened={configOpened}>

View File

@ -1,6 +1,6 @@
import {Observable} from "@babylonjs/core"; import {Observable} from "@babylonjs/core";
import {AppConfigType, LabelRenderingMode} from "./appConfigType"; import {AppConfigType, LabelRenderingMode} from "./appConfigType";
import log from "loglevel";
export class AppConfig { export class AppConfig {
public readonly onConfigChangedObservable = new Observable<AppConfigType>(); public readonly onConfigChangedObservable = new Observable<AppConfigType>();
private _currentConfig: AppConfigType; private _currentConfig: AppConfigType;
@ -89,41 +89,3 @@ export class AppConfig {
// Singleton instance for app-wide configuration // Singleton instance for app-wide configuration
// Use this instead of creating new AppConfig() instances // Use this instead of creating new AppConfig() instances
export const appConfigInstance = new AppConfig(); export const appConfigInstance = new AppConfig();
let defaultConfig: ConfigType =
{
locationSnap: '.1',
locationSnapEnabled: true,
rotationSnap: '90',
rotationSnapEnabled: true,
flyModeEnabled: true,
snapTurnSnap: '45',
snapTurnSnapEnabled: false
}
try {
const newConfig = JSON.parse(localStorage.getItem('config'));
defaultConfig = {...defaultConfig, ...newConfig};
} catch (e) {
}
export type ConfigType = {
locationSnap: string,
locationSnapEnabled: boolean,
rotationSnap: string,
rotationSnapEnabled: boolean,
flyModeEnabled: boolean,
snapTurnSnap: string,
snapTurnSnapEnabled: boolean
}
export function getAppConfig(): ConfigType {
return defaultConfig;
}
export function setAppConfig(config: ConfigType) {
const logger = log.getLogger('setAppConfig');
logger.debug('setting config', JSON.stringify(config));
localStorage.setItem('config', JSON.stringify(config));
}

View File

@ -3,7 +3,7 @@ import HavokPhysics from "@babylonjs/havok";
import {snapGridVal} from "./functions/snapGridVal"; import {snapGridVal} from "./functions/snapGridVal";
import {snapRotateVal} from "./functions/snapRotateVal"; import {snapRotateVal} from "./functions/snapRotateVal";
import {isDiagramEntity} from "../diagram/functions/isDiagramEntity"; import {isDiagramEntity} from "../diagram/functions/isDiagramEntity";
import {getAppConfig} from "./appConfig"; import {appConfigInstance} from "./appConfig";
export class CustomPhysics { export class CustomPhysics {
private readonly scene: Scene; private readonly scene: Scene;
@ -21,6 +21,7 @@ export class CustomPhysics {
scene.collisionsEnabled = true; scene.collisionsEnabled = true;
scene.onAfterPhysicsObservable.add(() => { scene.onAfterPhysicsObservable.add(() => {
const config = appConfigInstance.current;
scene.meshes.forEach((mesh) => { scene.meshes.forEach((mesh) => {
if (isDiagramEntity(mesh) && mesh.physicsBody) { if (isDiagramEntity(mesh) && mesh.physicsBody) {
const body = mesh.physicsBody; const body = mesh.physicsBody;
@ -29,18 +30,26 @@ export class CustomPhysics {
if (linearVelocity.length() < .1) { if (linearVelocity.length() < .1) {
body.disablePreStep = false; body.disablePreStep = false;
const pos: Vector3 = body.getObjectCenterWorld();
const val: Vector3 = snapGridVal(pos,
parseFloat(getAppConfig().locationSnap));
body.transformNode.position.set(val.x, val.y, val.z);
const rot: Quaternion =
Quaternion.FromEulerVector(
snapRotateVal(body.transformNode.rotationQuaternion.toEulerAngles(),
parseFloat(getAppConfig().rotationSnap)))
body.transformNode.rotationQuaternion.set( // Apply location snap if enabled
rot.x, rot.y, rot.z, rot.w if (config.locationSnap > 0) {
); const pos: Vector3 = body.getObjectCenterWorld();
const val: Vector3 = snapGridVal(pos, config.locationSnap);
body.transformNode.position.set(val.x, val.y, val.z);
}
// Apply rotation snap if enabled
if (config.rotateSnap > 0) {
const rot: Quaternion =
Quaternion.FromEulerVector(
snapRotateVal(body.transformNode.rotationQuaternion.toEulerAngles(),
config.rotateSnap));
body.transformNode.rotationQuaternion.set(
rot.x, rot.y, rot.z, rot.w
);
}
scene.onAfterRenderObservable.addOnce(() => { scene.onAfterRenderObservable.addOnce(() => {
body.disablePreStep = true; body.disablePreStep = true;
}); });

View File

@ -4,7 +4,7 @@ import {WebController} from "../../controllers/webController";
import {Rigplatform} from "../../controllers/rigplatform"; import {Rigplatform} from "../../controllers/rigplatform";
import {DiagramManager} from "../../diagram/diagramManager"; import {DiagramManager} from "../../diagram/diagramManager";
import {Spinner} from "../../objects/spinner"; import {Spinner} from "../../objects/spinner";
import {getAppConfig} from "../appConfig"; import {appConfigInstance} from "../appConfig";
import {Scene} from "@babylonjs/core"; import {Scene} from "@babylonjs/core";
@ -114,9 +114,9 @@ export async function groundMeshObserver(ground: AbstractMesh,
}); });
const rig = new Rigplatform(xr, diagramManager); const rig = new Rigplatform(xr, diagramManager);
const config = getAppConfig(); const config = appConfigInstance.current;
rig.flyMode = config.flyModeEnabled; rig.flyMode = config.flyMode;
rig.turnSnap = parseFloat(config.snapTurnSnap); rig.turnSnap = config.turnSnap;
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 // Set XR on diagram manager so toolbox can create exit button