Implement Phase 2 UI layout and toolbox integration for VR config panel

Phase 2: Add UI layout structure to VRConfigPanel
- Create 5 section containers with titles (Location Snap, Rotation Snap, Fly Mode, Snap Turn, Label Rendering Mode)
- Add visual separators between sections using Rectangle components
- Style with proper padding and spacing for VR readability (60px titles, blue #4A9EFF)
- Store section content containers as private properties for Phase 3-7 controls

Toolbox Integration (Phase 8 partial):
- Instantiate VRConfigPanel in DiagramMenuManager constructor
- Add "Config" button to toolbox (bottom-left, opposite Exit VR button)
- Wire up click handler to toggle panel visibility
- Add B-button positioning logic to reposition panel with other UI elements
- Pass DiagramMenuManager reference to Toolbox.setXR() for panel access

The panel now has complete skeleton structure and can be tested in VR:
- Click "Config" button on toolbox to show/hide panel
- Grab handle to reposition and test ergonomics
- Press B-button to auto-lower panel if too high
- 2m x 1.5m panel size optimized for VR viewing distance

🤖 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 12:03:24 -06:00
parent aa41895675
commit be311e6dc8
3 changed files with 128 additions and 6 deletions

View File

@ -12,12 +12,14 @@ import {GroupMenu} from "../menus/groupMenu";
import {ControllerEvent} from "../controllers/types/controllerEvent"; import {ControllerEvent} from "../controllers/types/controllerEvent";
import {ControllerEventType} from "../controllers/types/controllerEventType"; import {ControllerEventType} from "../controllers/types/controllerEventType";
import {ResizeGizmo} from "../gizmos/ResizeGizmo"; import {ResizeGizmo} from "../gizmos/ResizeGizmo";
import {VRConfigPanel} from "../menus/vrConfigPanel";
export class DiagramMenuManager { export class DiagramMenuManager {
public readonly toolbox: Toolbox; public readonly toolbox: Toolbox;
private readonly _notifier: Observable<DiagramEvent>; private readonly _notifier: Observable<DiagramEvent>;
private readonly _inputTextView: InputTextView; private readonly _inputTextView: InputTextView;
private readonly _vrConfigPanel: VRConfigPanel;
private _groupMenu: GroupMenu; private _groupMenu: GroupMenu;
private readonly _scene: Scene; private readonly _scene: Scene;
private _logger = log.getLogger('DiagramMenuManager'); private _logger = log.getLogger('DiagramMenuManager');
@ -29,6 +31,7 @@ export class DiagramMenuManager {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
this._notifier = notifier; this._notifier = notifier;
this._inputTextView = new InputTextView(controllerObservable); this._inputTextView = new InputTextView(controllerObservable);
this._vrConfigPanel = new VRConfigPanel(this._scene);
//this.configMenu = new ConfigMenu(config); //this.configMenu = new ConfigMenu(config);
this._inputTextView.onTextObservable.add((evt) => { this._inputTextView.onTextObservable.add((evt) => {
@ -62,10 +65,11 @@ export class DiagramMenuManager {
if (inputY > (cameraPos.y - .2)) { if (inputY > (cameraPos.y - .2)) {
this._inputTextView.handleMesh.position.y = localCamera.y - .2; this._inputTextView.handleMesh.position.y = localCamera.y - .2;
} }
const configY = this._inputTextView.handleMesh.absolutePosition.y;
/*if (configY > (cameraPos.y - .2)) { const configY = this._vrConfigPanel.handleMesh.absolutePosition.y;
this.configMenu.handleTransformNode.position.y = localCamera.y - .2; if (configY > (cameraPos.y - .2)) {
}*/ this._vrConfigPanel.handleMesh.position.y = localCamera.y - .2;
}
} }
} }
}); });
@ -159,6 +163,16 @@ export class DiagramMenuManager {
public setXR(xr: WebXRDefaultExperience): void { public setXR(xr: WebXRDefaultExperience): void {
this._xr = xr; this._xr = xr;
this.toolbox.setXR(xr); this.toolbox.setXR(xr, this);
}
public toggleVRConfigPanel(): void {
// Toggle visibility of VR config panel
const isEnabled = this._vrConfigPanel.handleMesh.isEnabled(false);
if (isEnabled) {
this._vrConfigPanel.hide();
} else {
this._vrConfigPanel.show();
}
} }
} }

View File

@ -1,5 +1,7 @@
import { import {
AdvancedDynamicTexture, AdvancedDynamicTexture,
Control,
Rectangle,
StackPanel, StackPanel,
TextBlock TextBlock
} from "@babylonjs/gui"; } from "@babylonjs/gui";
@ -41,6 +43,13 @@ export class VRConfigPanel {
private _configObserver: Observer<AppConfigType>; private _configObserver: Observer<AppConfigType>;
private _mainContainer: StackPanel; private _mainContainer: StackPanel;
// Section content containers (filled in Phases 3-7)
private _locationSnapContent: StackPanel;
private _rotationSnapContent: StackPanel;
private _flyModeContent: StackPanel;
private _snapTurnContent: StackPanel;
private _labelModeContent: StackPanel;
constructor(scene: Scene) { constructor(scene: Scene) {
this._scene = scene || DefaultScene.Scene; this._scene = scene || DefaultScene.Scene;
this._logger.debug('VRConfigPanel constructor called'); this._logger.debug('VRConfigPanel constructor called');
@ -188,12 +197,89 @@ export class VRConfigPanel {
title.paddingBottom = "40px"; title.paddingBottom = "40px";
this._mainContainer.addControl(title); this._mainContainer.addControl(title);
// Build configuration sections
this.buildConfigSections();
// Parent handle to platform when available // Parent handle to platform when available
this.setupPlatformParenting(); this.setupPlatformParenting();
this._logger.debug('VR config panel built successfully'); this._logger.debug('VR config panel built successfully');
} }
/**
* Build all configuration sections with layout structure
*/
private buildConfigSections(): void {
// Section 1: Location Snap
this._locationSnapContent = this.createSectionContainer("Location Snap");
this.addSeparator();
// Section 2: Rotation Snap
this._rotationSnapContent = this.createSectionContainer("Rotation Snap");
this.addSeparator();
// Section 3: Fly Mode
this._flyModeContent = this.createSectionContainer("Fly Mode");
this.addSeparator();
// Section 4: Snap Turn
this._snapTurnContent = this.createSectionContainer("Snap Turn");
this.addSeparator();
// Section 5: Label Rendering Mode
this._labelModeContent = this.createSectionContainer("Label Rendering Mode");
}
/**
* Create a section container with title
*/
private createSectionContainer(sectionTitle: string): StackPanel {
// Create section container
const section = new StackPanel(`section_${sectionTitle.replace(/\s+/g, '_')}`);
section.isVertical = true;
section.width = "100%";
section.height = "auto";
section.paddingTop = "20px";
section.paddingBottom = "20px";
this._mainContainer.addControl(section);
// Add section title
const titleText = new TextBlock(`title_${sectionTitle.replace(/\s+/g, '_')}`, sectionTitle);
titleText.height = "60px";
titleText.fontSize = 60;
titleText.color = "#4A9EFF"; // Bright blue for section titles
titleText.textHorizontalAlignment = TextBlock.HORIZONTAL_ALIGNMENT_LEFT;
titleText.paddingLeft = "20px";
titleText.paddingBottom = "10px";
section.addControl(titleText);
// Create content container for controls (to be filled in subsequent phases)
const contentContainer = new StackPanel(`content_${sectionTitle.replace(/\s+/g, '_')}`);
contentContainer.isVertical = true;
contentContainer.width = "100%";
contentContainer.height = "auto";
contentContainer.paddingLeft = "40px";
contentContainer.paddingRight = "40px";
section.addControl(contentContainer);
return contentContainer;
}
/**
* Add a visual separator line between sections
*/
private addSeparator(): void {
const separator = new Rectangle(`separator_${Date.now()}`);
separator.height = "2px";
separator.width = "90%";
separator.thickness = 0;
separator.background = "#444444"; // Dark gray separator
separator.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
separator.paddingTop = "10px";
separator.paddingBottom = "10px";
this._mainContainer.addControl(separator);
}
/** /**
* Set up parenting to platform for world movement tracking * Set up parenting to platform for world movement tracking
*/ */

View File

@ -30,6 +30,7 @@ export class Toolbox {
private readonly _scene: Scene; private readonly _scene: Scene;
private _xr?: WebXRDefaultExperience; private _xr?: WebXRDefaultExperience;
private _renderModeDisplay?: Button; private _renderModeDisplay?: Button;
private _diagramMenuManager?: any; // Import would create circular dependency
constructor(readyObservable: Observable<boolean>) { constructor(readyObservable: Observable<boolean>) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
@ -48,8 +49,9 @@ export class Toolbox {
Toolbox._instance = this; Toolbox._instance = this;
} }
public setXR(xr: WebXRDefaultExperience): void { public setXR(xr: WebXRDefaultExperience, diagramMenuManager?: any): void {
this._xr = xr; this._xr = xr;
this._diagramMenuManager = diagramMenuManager;
this.setupXRButton(); this.setupXRButton();
} }
private index = 0; private index = 0;
@ -176,6 +178,26 @@ export class Toolbox {
} }
}); });
// Create config button next to exit button
if (this._diagramMenuManager) {
const configButton = Button.CreateButton("config", "config", this._scene, {});
// Position button at bottom-left of toolbox, opposite the exit button
configButton.transform.position.x = -0.5; // Left side
configButton.transform.position.y = -0.35; // Below color grid (same as exit)
configButton.transform.position.z = 0; // Coplanar with toolbox
configButton.transform.rotation.y = Math.PI; // Flip 180° to face correctly
configButton.transform.scaling = new Vector3(.2, .2, .2); // Match exit button size
configButton.transform.parent = this._toolboxBaseNode;
configButton.onPointerObservable.add((evt) => {
this._logger.debug('Config button clicked', evt);
if (evt.sourceEvent.type == 'pointerdown') {
this._diagramMenuManager.toggleVRConfigPanel();
}
});
}
// Create rendering mode button that cycles through modes // Create rendering mode button that cycles through modes
this.createRenderModeButton(); this.createRenderModeButton();
} }