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>
10 KiB
10 KiB
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.tsfile - Implement class structure following Handle pattern:
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 implementationsrc/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):
- Location Snap
- Rotation Snap
- Fly Mode
- Snap Turn
- Label Rendering Mode
- Style containers with padding and spacing
- Add visual separators between sections
ADT Components to Use:
StackPanel- Main vertical containerTextBlock- Labels and section titlesRectangle- 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
appConfigInstanceon 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 switchRadioButton+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.tsto 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 creationsrc/util/functions/groundMeshObserver.ts:127-222- Component positioning
Phase 9: Observable Integration
- Subscribe to
appConfigInstance.onConfigChangedObservablein 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:
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
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
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
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
_handleMeshas root for entire panel UI
Reference Files
- src/menus/configMenu.ts - Existing VR config implementation with ADT
- src/menus/inputTextView.ts - Handle pattern and ADT setup
- src/react/pages/configModal.tsx - UI structure and config sections
- src/util/appConfig.ts - Config singleton and setter methods
- src/diagram/diagramMenuManager.ts - Toolbox button creation
- 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