Implement Phase 3: Location Snap controls for VR config panel

Add fully functional Location Snap controls:
- Toggle button (Enabled/Disabled) with blue/gray color coding
- 5 snap value buttons: 1cm, 5cm, 10cm, 50cm, 1m
- Selected button highlighted in blue with bold text
- Disabled appearance when snap is off (50% opacity)
- Wire up to appConfigInstance.setGridSnap()
- Update UI from config observable changes

Fix layout issues:
- Change texture aspect ratio from 2048x2048 to 2048x1536 (4:3) to match plane dimensions
- Add adaptHeightToChildren to section containers for proper auto-sizing
- Add horizontal alignment to button containers

🤖 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:13:22 -06:00
parent be311e6dc8
commit 5889a1ed79

View File

@ -1,5 +1,6 @@
import { import {
AdvancedDynamicTexture, AdvancedDynamicTexture,
Button,
Control, Control,
Rectangle, Rectangle,
StackPanel, StackPanel,
@ -50,6 +51,11 @@ export class VRConfigPanel {
private _snapTurnContent: StackPanel; private _snapTurnContent: StackPanel;
private _labelModeContent: StackPanel; private _labelModeContent: StackPanel;
// Location Snap UI controls
private _locationSnapEnabled: boolean = true;
private _locationSnapToggle: Button;
private _locationSnapButtons: Map<number, Button> = new Map();
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');
@ -170,10 +176,11 @@ export class VRConfigPanel {
this._panelMesh.material = material; this._panelMesh.material = material;
// Create AdvancedDynamicTexture with high resolution for crisp text in VR // Create AdvancedDynamicTexture with high resolution for crisp text in VR
// Match aspect ratio of plane (2m x 1.5m = 4:3)
this._advancedTexture = AdvancedDynamicTexture.CreateForMesh( this._advancedTexture = AdvancedDynamicTexture.CreateForMesh(
this._panelMesh, this._panelMesh,
2048, // Width in pixels 2048, // Width in pixels
2048 // Height in pixels (square for now, will adjust if needed) 1536 // Height in pixels (4:3 aspect ratio)
); );
// Create main container (vertical stack) // Create main container (vertical stack)
@ -212,6 +219,7 @@ export class VRConfigPanel {
private buildConfigSections(): void { private buildConfigSections(): void {
// Section 1: Location Snap // Section 1: Location Snap
this._locationSnapContent = this.createSectionContainer("Location Snap"); this._locationSnapContent = this.createSectionContainer("Location Snap");
this.buildLocationSnapControls();
this.addSeparator(); this.addSeparator();
// Section 2: Rotation Snap // Section 2: Rotation Snap
@ -238,7 +246,7 @@ export class VRConfigPanel {
const section = new StackPanel(`section_${sectionTitle.replace(/\s+/g, '_')}`); const section = new StackPanel(`section_${sectionTitle.replace(/\s+/g, '_')}`);
section.isVertical = true; section.isVertical = true;
section.width = "100%"; section.width = "100%";
section.height = "auto"; section.adaptHeightToChildren = true;
section.paddingTop = "20px"; section.paddingTop = "20px";
section.paddingBottom = "20px"; section.paddingBottom = "20px";
this._mainContainer.addControl(section); this._mainContainer.addControl(section);
@ -257,7 +265,7 @@ export class VRConfigPanel {
const contentContainer = new StackPanel(`content_${sectionTitle.replace(/\s+/g, '_')}`); const contentContainer = new StackPanel(`content_${sectionTitle.replace(/\s+/g, '_')}`);
contentContainer.isVertical = true; contentContainer.isVertical = true;
contentContainer.width = "100%"; contentContainer.width = "100%";
contentContainer.height = "auto"; contentContainer.adaptHeightToChildren = true;
contentContainer.paddingLeft = "40px"; contentContainer.paddingLeft = "40px";
contentContainer.paddingRight = "40px"; contentContainer.paddingRight = "40px";
section.addControl(contentContainer); section.addControl(contentContainer);
@ -280,6 +288,125 @@ export class VRConfigPanel {
this._mainContainer.addControl(separator); this._mainContainer.addControl(separator);
} }
/**
* Build Location Snap controls
*/
private buildLocationSnapControls(): void {
const currentSnap = appConfigInstance.current.locationSnap;
this._locationSnapEnabled = currentSnap > 0;
// Create horizontal container for toggle button
const toggleContainer = new StackPanel("locationSnapToggleContainer");
toggleContainer.isVertical = false;
toggleContainer.width = "100%";
toggleContainer.height = "80px";
toggleContainer.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
toggleContainer.paddingBottom = "20px";
this._locationSnapContent.addControl(toggleContainer);
// Create toggle button
this._locationSnapToggle = Button.CreateSimpleButton(
"locationSnapToggle",
this._locationSnapEnabled ? "Enabled" : "Disabled"
);
this._locationSnapToggle.width = "300px";
this._locationSnapToggle.height = "70px";
this._locationSnapToggle.fontSize = 48;
this._locationSnapToggle.color = "white";
this._locationSnapToggle.background = this._locationSnapEnabled ? "#4A9EFF" : "#666666";
this._locationSnapToggle.thickness = 0;
this._locationSnapToggle.cornerRadius = 10;
toggleContainer.addControl(this._locationSnapToggle);
// Toggle button click handler
this._locationSnapToggle.onPointerClickObservable.add(() => {
this._locationSnapEnabled = !this._locationSnapEnabled;
this._locationSnapToggle.textBlock.text = this._locationSnapEnabled ? "Enabled" : "Disabled";
this._locationSnapToggle.background = this._locationSnapEnabled ? "#4A9EFF" : "#666666";
if (this._locationSnapEnabled) {
// Re-enable with last selected value or default
const lastValue = appConfigInstance.current.locationSnap || 0.1;
appConfigInstance.setGridSnap(lastValue > 0 ? lastValue : 0.1);
this.updateLocationSnapButtonStates(lastValue);
} else {
// Disable by setting to 0
appConfigInstance.setGridSnap(0);
this.updateLocationSnapButtonStates(0);
}
});
// Create horizontal container for snap value buttons
const valuesContainer = new StackPanel("locationSnapValuesContainer");
valuesContainer.isVertical = false;
valuesContainer.width = "100%";
valuesContainer.height = "80px";
valuesContainer.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
this._locationSnapContent.addControl(valuesContainer);
// Define snap values
const snapValues = [
{ value: 0.01, label: "1cm" },
{ value: 0.05, label: "5cm" },
{ value: 0.1, label: "10cm" },
{ value: 0.5, label: "50cm" },
{ value: 1.0, label: "1m" }
];
// Create button for each snap value
snapValues.forEach((snap, index) => {
const isSelected = this._locationSnapEnabled && Math.abs(currentSnap - snap.value) < 0.001;
const btn = Button.CreateSimpleButton(`locationSnap_${snap.value}`, snap.label);
btn.width = "120px";
btn.height = "70px";
btn.fontSize = 42;
btn.color = "white";
btn.paddingRight = "10px";
btn.thickness = 0;
btn.cornerRadius = 8;
// Set initial appearance
if (isSelected) {
btn.background = "#4A9EFF";
btn.fontWeight = "bold";
} else {
btn.background = this._locationSnapEnabled ? "#333333" : "#222222";
btn.alpha = this._locationSnapEnabled ? 1.0 : 0.5;
}
// Click handler
btn.onPointerClickObservable.add(() => {
if (this._locationSnapEnabled) {
appConfigInstance.setGridSnap(snap.value);
this.updateLocationSnapButtonStates(snap.value);
}
});
this._locationSnapButtons.set(snap.value, btn);
valuesContainer.addControl(btn);
});
}
/**
* Update Location Snap button visual states
*/
private updateLocationSnapButtonStates(selectedValue: number): void {
this._locationSnapButtons.forEach((btn, value) => {
const isSelected = this._locationSnapEnabled && Math.abs(selectedValue - value) < 0.001;
if (isSelected) {
btn.background = "#4A9EFF";
btn.fontWeight = "bold";
} else {
btn.background = this._locationSnapEnabled ? "#333333" : "#222222";
btn.fontWeight = "normal";
}
btn.alpha = this._locationSnapEnabled ? 1.0 : 0.5;
});
}
/** /**
* Set up parenting to platform for world movement tracking * Set up parenting to platform for world movement tracking
*/ */
@ -307,11 +434,15 @@ export class VRConfigPanel {
private updateUIFromConfig(config: AppConfigType): void { private updateUIFromConfig(config: AppConfigType): void {
this._logger.debug('Updating UI from config', config); this._logger.debug('Updating UI from config', config);
// UI update logic will be implemented in subsequent phases // Update Location Snap UI
// For now, just log the config change if (this._locationSnapToggle && this._locationSnapButtons.size > 0) {
this._locationSnapEnabled = config.locationSnap > 0;
this._locationSnapToggle.textBlock.text = this._locationSnapEnabled ? "Enabled" : "Disabled";
this._locationSnapToggle.background = this._locationSnapEnabled ? "#4A9EFF" : "#666666";
this.updateLocationSnapButtonStates(config.locationSnap);
}
// Phase 3-7 will add: // Phase 4-7 will add:
// - Location snap UI update
// - Rotation snap UI update // - Rotation snap UI update
// - Fly mode UI update // - Fly mode UI update
// - Snap turn UI update // - Snap turn UI update