From be311e6dc8694bb277cd5e704a4d86f2686b684e Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Tue, 18 Nov 2025 12:03:24 -0600 Subject: [PATCH] Implement Phase 2 UI layout and toolbox integration for VR config panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/diagram/diagramMenuManager.ts | 24 +++++++-- src/menus/vrConfigPanel.ts | 86 +++++++++++++++++++++++++++++++ src/toolbox/toolbox.ts | 24 ++++++++- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/src/diagram/diagramMenuManager.ts b/src/diagram/diagramMenuManager.ts index c0a7d08..4e491a4 100644 --- a/src/diagram/diagramMenuManager.ts +++ b/src/diagram/diagramMenuManager.ts @@ -12,12 +12,14 @@ import {GroupMenu} from "../menus/groupMenu"; import {ControllerEvent} from "../controllers/types/controllerEvent"; import {ControllerEventType} from "../controllers/types/controllerEventType"; import {ResizeGizmo} from "../gizmos/ResizeGizmo"; +import {VRConfigPanel} from "../menus/vrConfigPanel"; export class DiagramMenuManager { public readonly toolbox: Toolbox; private readonly _notifier: Observable; private readonly _inputTextView: InputTextView; + private readonly _vrConfigPanel: VRConfigPanel; private _groupMenu: GroupMenu; private readonly _scene: Scene; private _logger = log.getLogger('DiagramMenuManager'); @@ -29,6 +31,7 @@ export class DiagramMenuManager { this._scene = DefaultScene.Scene; this._notifier = notifier; this._inputTextView = new InputTextView(controllerObservable); + this._vrConfigPanel = new VRConfigPanel(this._scene); //this.configMenu = new ConfigMenu(config); this._inputTextView.onTextObservable.add((evt) => { @@ -62,10 +65,11 @@ export class DiagramMenuManager { if (inputY > (cameraPos.y - .2)) { this._inputTextView.handleMesh.position.y = localCamera.y - .2; } - const configY = this._inputTextView.handleMesh.absolutePosition.y; - /*if (configY > (cameraPos.y - .2)) { - this.configMenu.handleTransformNode.position.y = localCamera.y - .2; - }*/ + + const configY = this._vrConfigPanel.handleMesh.absolutePosition.y; + 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 { 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(); + } } } \ No newline at end of file diff --git a/src/menus/vrConfigPanel.ts b/src/menus/vrConfigPanel.ts index 4a7d36e..cd43b41 100644 --- a/src/menus/vrConfigPanel.ts +++ b/src/menus/vrConfigPanel.ts @@ -1,5 +1,7 @@ import { AdvancedDynamicTexture, + Control, + Rectangle, StackPanel, TextBlock } from "@babylonjs/gui"; @@ -41,6 +43,13 @@ export class VRConfigPanel { private _configObserver: Observer; 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) { this._scene = scene || DefaultScene.Scene; this._logger.debug('VRConfigPanel constructor called'); @@ -188,12 +197,89 @@ export class VRConfigPanel { title.paddingBottom = "40px"; this._mainContainer.addControl(title); + // Build configuration sections + this.buildConfigSections(); + // Parent handle to platform when available this.setupPlatformParenting(); 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 */ diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 1b8749f..ceb91fb 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -30,6 +30,7 @@ export class Toolbox { private readonly _scene: Scene; private _xr?: WebXRDefaultExperience; private _renderModeDisplay?: Button; + private _diagramMenuManager?: any; // Import would create circular dependency constructor(readyObservable: Observable) { this._scene = DefaultScene.Scene; @@ -48,8 +49,9 @@ export class Toolbox { Toolbox._instance = this; } - public setXR(xr: WebXRDefaultExperience): void { + public setXR(xr: WebXRDefaultExperience, diagramMenuManager?: any): void { this._xr = xr; + this._diagramMenuManager = diagramMenuManager; this.setupXRButton(); } 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 this.createRenderModeButton(); }