Add VR configuration panel implementation plan and Phase 1 foundation
Create VRCONFIGPLAN.md with comprehensive 10-phase implementation guide for building an immersive WebXR configuration panel using AdvancedDynamicTexture. Implement Phase 1: Core panel setup - Create VRConfigPanel class following Handle pattern for grabbability - Set up 2m x 1.5m plane mesh with high-resolution ADT (2048x2048) - Initialize main StackPanel container with title - Add show/hide/dispose methods for panel lifecycle - Integrate with appConfigInstance observable for config changes - Auto-parent to platform for world movement tracking The panel starts hidden and provides foundation for adding configuration controls in subsequent phases (location snap, rotation snap, fly mode, snap turn, label rendering mode). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
970f6fc78a
commit
aa41895675
297
VRCONFIGPLAN.md
Normal file
297
VRCONFIGPLAN.md
Normal file
@ -0,0 +1,297 @@
|
||||
# VR Configuration Panel Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Create an immersive WebXR configuration panel that mirrors the 2D ConfigModal functionality using BabylonJS AdvancedDynamicTexture (ADT). The panel will allow users to adjust all application settings directly in VR.
|
||||
|
||||
## Recommended Approach: AdvancedDynamicTexture (ADT)
|
||||
|
||||
**Why ADT?**
|
||||
- Most common approach for WebXR UI in BabylonJS
|
||||
- Existing pattern in codebase (see `src/menus/configMenu.ts`)
|
||||
- Good balance of simplicity and functionality
|
||||
- Native support for text, buttons, sliders, and dropdowns
|
||||
- Easy integration with existing Handle pattern
|
||||
|
||||
**Estimated Effort**: 150-200 lines of code, 4-8 hours implementation time
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/menus/
|
||||
├── vrConfigPanel.ts (NEW - main implementation)
|
||||
└── configMenu.ts (REFERENCE - existing VR config example)
|
||||
|
||||
src/diagram/
|
||||
└── diagramMenuManager.ts (MODIFY - add toolbox button)
|
||||
|
||||
src/util/
|
||||
└── appConfig.ts (USE - singleton for config management)
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Panel Setup
|
||||
- [ ] Create `src/menus/vrConfigPanel.ts` file
|
||||
- [ ] Implement class structure following Handle pattern:
|
||||
```typescript
|
||||
export class VRConfigPanel {
|
||||
private _scene: Scene;
|
||||
private _handleMesh: Mesh;
|
||||
private _advancedTexture: AdvancedDynamicTexture;
|
||||
private _configObserver: Observer<AppConfigType>;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
// Initialize panel
|
||||
}
|
||||
|
||||
public get handleMesh(): Mesh {
|
||||
return this._handleMesh;
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this._handleMesh.setEnabled(true);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this._handleMesh.setEnabled(false);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Create base mesh (plane) for panel backing
|
||||
- [ ] Set up AdvancedDynamicTexture with appropriate resolution (1024x1024 or 2048x2048)
|
||||
- [ ] Position panel at comfortable viewing distance (0.5-0.7m from camera)
|
||||
- [ ] Make panel grabbable via Handle pattern
|
||||
|
||||
**Reference Files**:
|
||||
- `src/menus/inputTextView.ts` - Handle pattern implementation
|
||||
- `src/menus/configMenu.ts` - ADT usage example
|
||||
|
||||
### Phase 2: UI Layout Structure
|
||||
- [ ] Create main container (StackPanel for vertical layout)
|
||||
- [ ] Add title text at top ("Configuration")
|
||||
- [ ] Create 5 section containers (one for each config group):
|
||||
1. Location Snap
|
||||
2. Rotation Snap
|
||||
3. Fly Mode
|
||||
4. Snap Turn
|
||||
5. Label Rendering Mode
|
||||
- [ ] Style containers with padding and spacing
|
||||
- [ ] Add visual separators between sections
|
||||
|
||||
**ADT Components to Use**:
|
||||
- `StackPanel` - Main vertical container
|
||||
- `TextBlock` - Labels and section titles
|
||||
- `Rectangle` - Containers and separators
|
||||
|
||||
**Reference**: `src/menus/configMenu.ts:44-89` for existing layout patterns
|
||||
|
||||
### Phase 3: Location Snap Section
|
||||
- [ ] Add "Location Snap" label
|
||||
- [ ] Create enable/disable toggle button
|
||||
- Shows "Enabled" or "Disabled"
|
||||
- Updates `appConfigInstance` on click
|
||||
- [ ] Add RadioGroup for snap values:
|
||||
- Options: 1cm (.01), 5cm (.05), 10cm (.1), 50cm (.5), 1m (1)
|
||||
- Default: 10cm (.1)
|
||||
- Disable when snap is off
|
||||
- [ ] Wire up to `appConfigInstance.setGridSnap(value)`
|
||||
- [ ] Subscribe to config changes to update UI
|
||||
|
||||
**ADT Components**:
|
||||
- `Button` - Toggle switch
|
||||
- `RadioButton` + `TextBlock` - Value selection
|
||||
- Color coding: enabled (green/myColor), disabled (gray)
|
||||
|
||||
**Reference ConfigModal**: `src/react/pages/configModal.tsx:83-94`
|
||||
|
||||
### Phase 4: Rotation Snap Section
|
||||
- [ ] Add "Rotation Snap" label
|
||||
- [ ] Create enable/disable toggle button
|
||||
- [ ] Add RadioGroup for rotation values:
|
||||
- Options: 22.5°, 45°, 90°, 180°, 360°
|
||||
- Default: 90°
|
||||
- Disable when snap is off
|
||||
- [ ] Wire up to `appConfigInstance.setRotateSnap(value)`
|
||||
- [ ] Subscribe to config changes to update UI
|
||||
|
||||
**Reference ConfigModal**: `src/react/pages/configModal.tsx:96-108`
|
||||
|
||||
### Phase 5: Fly Mode Section
|
||||
- [ ] Add "Fly Mode" label
|
||||
- [ ] Create toggle button
|
||||
- Shows "Fly Mode Enabled" or "Fly Mode Disabled"
|
||||
- [ ] Wire up to `appConfigInstance.setFlyMode(value)`
|
||||
- [ ] Subscribe to config changes to update UI
|
||||
|
||||
**Reference ConfigModal**: `src/react/pages/configModal.tsx:109-112`
|
||||
|
||||
### Phase 6: Snap Turn Section
|
||||
- [ ] Add "Snap Turn" label
|
||||
- [ ] Create enable/disable toggle button
|
||||
- [ ] Add RadioGroup for snap turn angles:
|
||||
- Options: 22.5°, 45°, 90°, 180°, 360°
|
||||
- Default: 45°
|
||||
- Disable when snap is off
|
||||
- [ ] Wire up to `appConfigInstance.setTurnSnap(value)`
|
||||
- [ ] Subscribe to config changes to update UI
|
||||
|
||||
**Reference ConfigModal**: `src/react/pages/configModal.tsx:113-125`
|
||||
|
||||
### Phase 7: Label Rendering Mode Section
|
||||
- [ ] Add "Label Rendering Mode" label
|
||||
- [ ] Create RadioGroup for rendering modes:
|
||||
- Fixed
|
||||
- Billboard (Always Face Camera)
|
||||
- Dynamic (Coming Soon) - disabled
|
||||
- Distance-based (Coming Soon) - disabled
|
||||
- [ ] Wire up to `appConfigInstance.setLabelRenderingMode(value)`
|
||||
- [ ] Subscribe to config changes to update UI
|
||||
- [ ] Style disabled options with gray text
|
||||
|
||||
**Reference ConfigModal**: `src/react/pages/configModal.tsx:126-135`
|
||||
|
||||
### Phase 8: Integration with Toolbox
|
||||
- [ ] Modify `src/diagram/diagramMenuManager.ts` to instantiate VRConfigPanel
|
||||
- [ ] Add "Config" button to toolbox (similar to "Exit VR" button pattern)
|
||||
- [ ] Wire up button click to show/hide panel
|
||||
- [ ] Position panel relative to camera when shown (see `positionComponentsRelativeToCamera`)
|
||||
- [ ] Add parent relationship to platform for movement tracking
|
||||
|
||||
**Reference**:
|
||||
- `src/diagram/diagramMenuManager.ts:85-97` - Exit button creation
|
||||
- `src/util/functions/groundMeshObserver.ts:127-222` - Component positioning
|
||||
|
||||
### Phase 9: Observable Integration
|
||||
- [ ] Subscribe to `appConfigInstance.onConfigChangedObservable` in constructor
|
||||
- [ ] Update all UI elements when config changes externally
|
||||
- [ ] Ensure Observable cleanup in dispose() method
|
||||
- [ ] Test config changes from both VR panel and 2D ConfigModal
|
||||
|
||||
**Pattern**:
|
||||
```typescript
|
||||
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
|
||||
// Update UI elements to reflect new config
|
||||
this.updateLocationSnapUI(config.locationSnap);
|
||||
this.updateRotationSnapUI(config.rotateSnap);
|
||||
// ... etc
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 10: Testing & Polish
|
||||
- [ ] Test all toggle switches update config correctly
|
||||
- [ ] Test all radio button selections update config correctly
|
||||
- [ ] Verify config changes propagate to DiagramObjects (label mode, snap behavior)
|
||||
- [ ] Test panel positioning in VR (comfortable viewing distance)
|
||||
- [ ] Test panel grabbability via Handle
|
||||
- [ ] Verify panel follows platform movement
|
||||
- [ ] Test config persistence (localStorage)
|
||||
- [ ] Test config synchronization between VR panel and 2D ConfigModal
|
||||
- [ ] Add visual feedback for button clicks (color changes, animations)
|
||||
- [ ] Ensure proper cleanup on panel disposal
|
||||
- [ ] Test in both WebXR and desktop modes
|
||||
|
||||
## Code Patterns to Follow
|
||||
|
||||
### 1. Toggle Button Pattern
|
||||
```typescript
|
||||
const toggleButton = Button.CreateSimpleButton("toggle", "Enabled");
|
||||
toggleButton.width = "200px";
|
||||
toggleButton.height = "40px";
|
||||
toggleButton.color = "white";
|
||||
toggleButton.background = "green";
|
||||
toggleButton.onPointerClickObservable.add(() => {
|
||||
const newValue = !currentValue;
|
||||
toggleButton.textBlock.text = newValue ? "Enabled" : "Disabled";
|
||||
toggleButton.background = newValue ? "green" : "gray";
|
||||
appConfigInstance.setSomeSetting(newValue);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. RadioGroup Pattern
|
||||
```typescript
|
||||
const radioGroup = new SelectionPanel("snapValues");
|
||||
const options = [
|
||||
{ value: 0.01, label: "1cm" },
|
||||
{ value: 0.1, label: "10cm" },
|
||||
// ... more options
|
||||
];
|
||||
|
||||
options.forEach(option => {
|
||||
const radio = new RadioButton();
|
||||
radio.width = "20px";
|
||||
radio.height = "20px";
|
||||
radio.isChecked = (option.value === currentValue);
|
||||
radio.onIsCheckedChangedObservable.add((checked) => {
|
||||
if (checked) {
|
||||
appConfigInstance.setGridSnap(option.value);
|
||||
}
|
||||
});
|
||||
// Add label next to radio button
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Config Observer Pattern
|
||||
```typescript
|
||||
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
|
||||
this.updateUIFromConfig(config);
|
||||
});
|
||||
|
||||
// In dispose():
|
||||
if (this._configObserver) {
|
||||
appConfigInstance.onConfigChangedObservable.remove(this._configObserver);
|
||||
}
|
||||
```
|
||||
|
||||
## Key Integration Points
|
||||
|
||||
### AppConfig Singleton
|
||||
- Import: `import {appConfigInstance} from "../util/appConfig";`
|
||||
- Read: `appConfigInstance.current.locationSnap`
|
||||
- Write: `appConfigInstance.setGridSnap(0.1)`
|
||||
- Subscribe: `appConfigInstance.onConfigChangedObservable.add(callback)`
|
||||
|
||||
### DiagramMenuManager
|
||||
- Instantiate panel: `this._vrConfigPanel = new VRConfigPanel(this._scene);`
|
||||
- Add button to toolbox: Follow exit button pattern in `setupExitButton()`
|
||||
- Show panel: `this._vrConfigPanel.show();`
|
||||
- Position panel: Follow pattern in `groundMeshObserver.ts:127-222`
|
||||
|
||||
### Handle Pattern
|
||||
- Make panel grabbable by controllers
|
||||
- Parent to platform for world movement
|
||||
- Use `_handleMesh` as root for entire panel UI
|
||||
|
||||
## Reference Files
|
||||
|
||||
1. **src/menus/configMenu.ts** - Existing VR config implementation with ADT
|
||||
2. **src/menus/inputTextView.ts** - Handle pattern and ADT setup
|
||||
3. **src/react/pages/configModal.tsx** - UI structure and config sections
|
||||
4. **src/util/appConfig.ts** - Config singleton and setter methods
|
||||
5. **src/diagram/diagramMenuManager.ts** - Toolbox button creation
|
||||
6. **src/util/functions/groundMeshObserver.ts** - Component positioning
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All 5 config sections implemented and functional
|
||||
- [ ] Config changes in VR panel update appConfigInstance
|
||||
- [ ] Config changes propagate to all DiagramObjects
|
||||
- [ ] Panel is grabbable and repositionable
|
||||
- [ ] Panel follows platform movement
|
||||
- [ ] Config persists to localStorage
|
||||
- [ ] Synchronized with 2D ConfigModal
|
||||
- [ ] Comfortable viewing experience in VR
|
||||
- [ ] No memory leaks (proper Observable cleanup)
|
||||
|
||||
## Notes
|
||||
|
||||
- Start hidden (only show when user clicks toolbox button)
|
||||
- Position at ~0.5m in front of camera when opened
|
||||
- Use Y-axis billboard mode to keep panel upright but allow rotation
|
||||
- Consider adding "Close" button at bottom of panel
|
||||
- Match color scheme with existing UI (myColor theme)
|
||||
- Test with both left and right controller grabbing
|
||||
234
src/menus/vrConfigPanel.ts
Normal file
234
src/menus/vrConfigPanel.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import {
|
||||
AdvancedDynamicTexture,
|
||||
StackPanel,
|
||||
TextBlock
|
||||
} from "@babylonjs/gui";
|
||||
import {
|
||||
Color3,
|
||||
Mesh,
|
||||
MeshBuilder,
|
||||
Observer,
|
||||
Scene,
|
||||
StandardMaterial,
|
||||
TransformNode,
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
import {appConfigInstance} from "../util/appConfig";
|
||||
import {AppConfigType} from "../util/appConfigType";
|
||||
import log from "loglevel";
|
||||
import {DefaultScene} from "../defaultScene";
|
||||
import {Handle} from "../objects/handle";
|
||||
|
||||
/**
|
||||
* VRConfigPanel - Immersive WebXR configuration panel using AdvancedDynamicTexture
|
||||
*
|
||||
* Provides a VR-native interface for adjusting application settings including:
|
||||
* - Location snap settings
|
||||
* - Rotation snap settings
|
||||
* - Fly mode toggle
|
||||
* - Snap turn settings
|
||||
* - Label rendering mode
|
||||
*
|
||||
* The panel is grabbable via the Handle pattern and integrates with the AppConfig singleton.
|
||||
*/
|
||||
export class VRConfigPanel {
|
||||
private readonly _logger = log.getLogger('VRConfigPanel');
|
||||
private readonly _scene: Scene;
|
||||
private readonly _baseTransform: TransformNode;
|
||||
private _handle: Handle;
|
||||
private _panelMesh: Mesh;
|
||||
private _advancedTexture: AdvancedDynamicTexture;
|
||||
private _configObserver: Observer<AppConfigType>;
|
||||
private _mainContainer: StackPanel;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this._scene = scene || DefaultScene.Scene;
|
||||
this._logger.debug('VRConfigPanel constructor called');
|
||||
|
||||
// Create base transform for the entire panel hierarchy
|
||||
this._baseTransform = new TransformNode("vrConfigPanelBase", this._scene);
|
||||
|
||||
// Create handle for grabbing (Handle will become parent of baseTransform)
|
||||
this._handle = new Handle(
|
||||
this._baseTransform,
|
||||
'Configuration',
|
||||
new Vector3(0.5, 1.6, 0.4), // Default position relative to platform
|
||||
new Vector3(0.5, 0.6, 0) // Default rotation
|
||||
);
|
||||
|
||||
// Build the panel mesh and UI
|
||||
this.buildPanel();
|
||||
|
||||
// Subscribe to config changes
|
||||
this._configObserver = appConfigInstance.onConfigChangedObservable.add((config) => {
|
||||
this.updateUIFromConfig(config);
|
||||
});
|
||||
|
||||
// Start hidden - will be shown when user clicks toolbox button
|
||||
this.hide();
|
||||
|
||||
this._logger.debug('VRConfigPanel initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handle's transform node for external positioning
|
||||
*/
|
||||
public get handleMesh(): TransformNode {
|
||||
return this._handle.transformNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the configuration panel
|
||||
*/
|
||||
public show(): void {
|
||||
if (this._handle && this._handle.transformNode) {
|
||||
this._handle.transformNode.setEnabled(true);
|
||||
this._logger.debug('VRConfigPanel shown');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the configuration panel
|
||||
*/
|
||||
public hide(): void {
|
||||
if (this._handle && this._handle.transformNode) {
|
||||
this._handle.transformNode.setEnabled(false);
|
||||
this._logger.debug('VRConfigPanel hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._logger.debug('Disposing VRConfigPanel');
|
||||
|
||||
// Remove config observer
|
||||
if (this._configObserver) {
|
||||
appConfigInstance.onConfigChangedObservable.remove(this._configObserver);
|
||||
this._configObserver = null;
|
||||
}
|
||||
|
||||
// Dispose of ADT
|
||||
if (this._advancedTexture) {
|
||||
this._advancedTexture.dispose();
|
||||
this._advancedTexture = null;
|
||||
}
|
||||
|
||||
// Dispose of mesh
|
||||
if (this._panelMesh) {
|
||||
this._panelMesh.dispose();
|
||||
this._panelMesh = null;
|
||||
}
|
||||
|
||||
// Dispose of base transform
|
||||
if (this._baseTransform) {
|
||||
this._baseTransform.dispose();
|
||||
}
|
||||
|
||||
// Handle will be disposed via its parent
|
||||
this._handle = null;
|
||||
|
||||
this._logger.debug('VRConfigPanel disposed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the panel mesh and initialize AdvancedDynamicTexture
|
||||
*/
|
||||
private buildPanel(): void {
|
||||
this._logger.debug('Building VR config panel');
|
||||
|
||||
// Create panel plane mesh (2m wide x 1.5m tall for comfortable viewing in VR)
|
||||
this._panelMesh = MeshBuilder.CreatePlane(
|
||||
"vrConfigPanelPlane",
|
||||
{
|
||||
width: 2.0,
|
||||
height: 1.5
|
||||
},
|
||||
this._scene
|
||||
);
|
||||
|
||||
// Parent to base transform
|
||||
this._panelMesh.parent = this._baseTransform;
|
||||
|
||||
// Position slightly forward and up from handle
|
||||
this._panelMesh.position = new Vector3(0, 0.2, 0);
|
||||
|
||||
// Create material for panel backing
|
||||
const material = new StandardMaterial("vrConfigPanelMaterial", this._scene);
|
||||
material.diffuseColor = new Color3(0.1, 0.1, 0.15); // Dark blue-gray
|
||||
material.specularColor = new Color3(0.1, 0.1, 0.1);
|
||||
this._panelMesh.material = material;
|
||||
|
||||
// Create AdvancedDynamicTexture with high resolution for crisp text in VR
|
||||
this._advancedTexture = AdvancedDynamicTexture.CreateForMesh(
|
||||
this._panelMesh,
|
||||
2048, // Width in pixels
|
||||
2048 // Height in pixels (square for now, will adjust if needed)
|
||||
);
|
||||
|
||||
// Create main container (vertical stack)
|
||||
this._mainContainer = new StackPanel("vrConfigMainContainer");
|
||||
this._mainContainer.isVertical = true;
|
||||
this._mainContainer.width = "100%";
|
||||
this._mainContainer.height = "100%";
|
||||
this._mainContainer.paddingTop = "40px";
|
||||
this._mainContainer.paddingBottom = "40px";
|
||||
this._mainContainer.paddingLeft = "60px";
|
||||
this._mainContainer.paddingRight = "60px";
|
||||
this._advancedTexture.addControl(this._mainContainer);
|
||||
|
||||
// Add title
|
||||
const title = new TextBlock("vrConfigTitle", "Configuration");
|
||||
title.height = "120px";
|
||||
title.fontSize = 80;
|
||||
title.color = "white";
|
||||
title.textHorizontalAlignment = TextBlock.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
title.textVerticalAlignment = TextBlock.VERTICAL_ALIGNMENT_TOP;
|
||||
title.paddingBottom = "40px";
|
||||
this._mainContainer.addControl(title);
|
||||
|
||||
// Parent handle to platform when available
|
||||
this.setupPlatformParenting();
|
||||
|
||||
this._logger.debug('VR config panel built successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up parenting to platform for world movement tracking
|
||||
*/
|
||||
private setupPlatformParenting(): void {
|
||||
const platform = this._scene.getMeshById('platform');
|
||||
if (platform) {
|
||||
this._handle.transformNode.parent = platform;
|
||||
this._logger.debug('VRConfigPanel parented to existing platform');
|
||||
} else {
|
||||
// Wait for platform to be added
|
||||
const handler = this._scene.onNewMeshAddedObservable.add((mesh) => {
|
||||
if (mesh && mesh.id === 'platform') {
|
||||
this._handle.transformNode.parent = mesh;
|
||||
this._logger.debug('VRConfigPanel parented to newly added platform');
|
||||
this._scene.onNewMeshAddedObservable.remove(handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all UI elements to reflect current config
|
||||
* Called when config changes externally
|
||||
*/
|
||||
private updateUIFromConfig(config: AppConfigType): void {
|
||||
this._logger.debug('Updating UI from config', config);
|
||||
|
||||
// UI update logic will be implemented in subsequent phases
|
||||
// For now, just log the config change
|
||||
|
||||
// Phase 3-7 will add:
|
||||
// - Location snap UI update
|
||||
// - Rotation snap UI update
|
||||
// - Fly mode UI update
|
||||
// - Snap turn UI update
|
||||
// - Label rendering mode UI update
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user