diff --git a/src/objects/buttons/ConfigButton.ts b/src/objects/buttons/ConfigButton.ts new file mode 100644 index 0000000..7e537df --- /dev/null +++ b/src/objects/buttons/ConfigButton.ts @@ -0,0 +1,60 @@ +import {Scene, TransformNode, Vector3} from "@babylonjs/core"; +import {Button} from "../Button"; +import log, {Logger} from "loglevel"; + +/** + * Button that toggles the VR configuration panel + */ +export class ConfigButton { + private _button: Button; + private readonly _logger: Logger = log.getLogger('ConfigButton'); + + /** + * Creates a Config button + * @param toggleCallback Function to call when button is clicked + * @param scene BabylonJS scene + * @param parent Parent transform node to attach button to + * @param position Position relative to parent (default: bottom-left) + */ + constructor( + private readonly toggleCallback: () => void, + scene: Scene, + parent: TransformNode, + position: Vector3 = new Vector3(0.5, -0.35, 0) + ) { + this._button = Button.CreateButton("config", "config", scene, {}); + + // Position button + this._button.transform.position = position; + this._button.transform.rotation.y = Math.PI; // Flip 180° to face correctly + this._button.transform.scaling = new Vector3(0.2, 0.2, 0.2); + this._button.transform.parent = parent; + + // Add click handler + this._button.onPointerObservable.add((evt) => { + if (evt.sourceEvent.type === 'pointerdown') { + this._logger.debug('Config button clicked'); + this.toggleCallback(); + } + }); + + this._logger.debug('ConfigButton created'); + } + + /** + * Get the button transform for external positioning/manipulation + */ + public get transform(): TransformNode { + return this._button.transform; + } + + /** + * Dispose of the button and clean up resources + */ + public dispose(): void { + if (this._button) { + this._button.dispose(); + } + this._logger.debug('ConfigButton disposed'); + } +} diff --git a/src/objects/buttons/ExitXRButton.ts b/src/objects/buttons/ExitXRButton.ts new file mode 100644 index 0000000..daa6a7a --- /dev/null +++ b/src/objects/buttons/ExitXRButton.ts @@ -0,0 +1,60 @@ +import {Scene, TransformNode, Vector3, WebXRDefaultExperience} from "@babylonjs/core"; +import {Button} from "../Button"; +import log, {Logger} from "loglevel"; + +/** + * Button that exits the XR session when clicked + */ +export class ExitXRButton { + private _button: Button; + private readonly _logger: Logger = log.getLogger('ExitXRButton'); + + /** + * Creates an Exit XR button + * @param xr WebXR experience to exit from + * @param scene BabylonJS scene + * @param parent Parent transform node to attach button to + * @param position Position relative to parent (default: bottom-right) + */ + constructor( + private readonly xr: WebXRDefaultExperience, + scene: Scene, + parent: TransformNode, + position: Vector3 = new Vector3(-0.5, -0.35, 0) + ) { + this._button = Button.CreateButton("exitXr", "exitXr", scene, {}); + + // Position button + this._button.transform.position = position; + this._button.transform.rotation.y = Math.PI; // Flip 180° to face correctly + this._button.transform.scaling = new Vector3(0.2, 0.2, 0.2); + this._button.transform.parent = parent; + + // Add click handler + this._button.onPointerObservable.add((evt) => { + if (evt.sourceEvent.type === 'pointerdown') { + this._logger.debug('Exit XR button clicked'); + this.xr.baseExperience.exitXRAsync(); + } + }); + + this._logger.debug('ExitXRButton created'); + } + + /** + * Get the button transform for external positioning/manipulation + */ + public get transform(): TransformNode { + return this._button.transform; + } + + /** + * Dispose of the button and clean up resources + */ + public dispose(): void { + if (this._button) { + this._button.dispose(); + } + this._logger.debug('ExitXRButton disposed'); + } +} diff --git a/src/objects/buttons/RenderModeButton.ts b/src/objects/buttons/RenderModeButton.ts new file mode 100644 index 0000000..898b78e --- /dev/null +++ b/src/objects/buttons/RenderModeButton.ts @@ -0,0 +1,129 @@ +import {Color3, Scene, TransformNode, Vector3} from "@babylonjs/core"; +import {Button} from "../Button"; +import {LightmapGenerator} from "../../util/lightmapGenerator"; +import {RenderingMode, RenderingModeLabels} from "../../util/renderingMode"; +import log, {Logger} from "loglevel"; + +/** + * Button that cycles through rendering modes + */ +export class RenderModeButton { + private _button: Button; + private readonly _logger: Logger = log.getLogger('RenderModeButton'); + private readonly _scene: Scene; + private readonly _parent: TransformNode; + private readonly _position: Vector3; + private readonly _scaling: Vector3; + + private readonly _modes: RenderingMode[] = [ + RenderingMode.LIGHTMAP_WITH_LIGHTING, + RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE, + RenderingMode.FLAT_EMISSIVE, + RenderingMode.DIFFUSE_WITH_LIGHTS + ]; + + /** + * Creates a Render Mode button + * @param scene BabylonJS scene + * @param parent Parent transform node to attach button to + * @param position Position relative to parent (default: center below) + * @param scaling Scaling for the button (default: 0.4, 0.4, 0.4) + */ + constructor( + scene: Scene, + parent: TransformNode, + position: Vector3 = new Vector3(0, -0.2, 0), + scaling: Vector3 = new Vector3(0.4, 0.4, 0.4) + ) { + this._scene = scene; + this._parent = parent; + this._position = position; + this._scaling = scaling; + + this.createButton(); + this._logger.debug('RenderModeButton created'); + } + + /** + * Create or recreate the button with current mode label + * @private + */ + private createButton(): void { + const currentMode = LightmapGenerator.getRenderingMode(); + + this._button = Button.CreateButton( + `Mode: ${RenderingModeLabels[currentMode]}`, + 'renderModeButton', + this._scene, + { + width: 0.5, + height: 0.2, + background: Color3.FromHexString("#333333"), + color: Color3.White(), + fontSize: 240 + } + ); + + // Position button + this._button.transform.position = this._position; + this._button.transform.rotation.y = Math.PI; + this._button.transform.scaling = this._scaling; + this._button.transform.parent = this._parent; + + // Add click handler to cycle through modes + this._button.onPointerObservable.add((evt) => { + if (evt.sourceEvent.type === 'pointerdown') { + this.cycleRenderMode(); + } + }); + } + + /** + * Cycle to the next rendering mode + * @private + */ + private cycleRenderMode(): void { + const currentMode = LightmapGenerator.getRenderingMode(); + const currentIndex = this._modes.indexOf(currentMode); + const nextIndex = (currentIndex + 1) % this._modes.length; + const nextMode = this._modes[nextIndex]; + + this._logger.info(`Cycling to rendering mode: ${nextMode}`); + LightmapGenerator.updateAllMaterials(this._scene, nextMode); + + // Recreate button with new label + this.updateButton(nextMode); + } + + /** + * Update button with new rendering mode label + * @param mode New rendering mode + * @private + */ + private updateButton(mode: RenderingMode): void { + // Dispose old button + if (this._button) { + this._button.dispose(); + } + + // Create new button with updated text + this.createButton(); + } + + /** + * Get the button transform for external positioning/manipulation + */ + public get transform(): TransformNode { + return this._button.transform; + } + + /** + * Dispose of the button and clean up resources + */ + public dispose(): void { + if (this._button) { + this._button.dispose(); + } + this._logger.debug('RenderModeButton disposed'); + } +} diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 13fb740..c22cf1b 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -3,10 +3,11 @@ import {buildColor} from "./functions/buildColor"; import log from "loglevel"; import {Handle} from "../objects/handle"; import {DefaultScene} from "../defaultScene"; -import {Button} from "../objects/Button"; -import {LightmapGenerator} from "../util/lightmapGenerator"; -import {RenderingMode, RenderingModeLabels} from "../util/renderingMode"; import {AnimatedLineTexture} from "../util/animatedLineTexture"; +import {ExitXRButton} from "../objects/buttons/ExitXRButton"; +import {ConfigButton} from "../objects/buttons/ConfigButton"; +import {RenderModeButton} from "../objects/buttons/RenderModeButton"; +import {LightmapGenerator} from "../util/lightmapGenerator"; const colors: string[] = [ "#222222", "#8b4513", "#006400", "#778899", @@ -30,9 +31,13 @@ export class Toolbox { private readonly _handle: Handle; private readonly _scene: Scene; private _xr?: WebXRDefaultExperience; - private _renderModeDisplay?: Button; private _diagramMenuManager?: any; // Import would create circular dependency + // Button instances + private _exitXRButton?: ExitXRButton; + private _configButton?: ConfigButton; + private _renderModeButton?: RenderModeButton; + constructor(readyObservable: Observable) { this._scene = DefaultScene.Scene; this._toolboxBaseNode = new TransformNode("toolbox", this._scene); @@ -145,145 +150,33 @@ export class Toolbox { this._xr.baseExperience.onStateChangedObservable.add((state) => { if (state == 2) { // WebXRState.IN_XR // Create exit XR button - const exitButton = Button.CreateButton("exitXr", "exitXr", this._scene, {}); + this._exitXRButton = new ExitXRButton( + this._xr, + this._scene, + this._toolboxBaseNode, + new Vector3(-0.5, -0.35, 0) // Bottom-right + ); - // Position button at bottom-right of toolbox, matching handle size and orientation - exitButton.transform.position.x = -0.5; // Right side - exitButton.transform.position.y = -0.35; // Below color grid - exitButton.transform.position.z = 0; // Coplanar with toolbox - exitButton.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly - exitButton.transform.scaling = new Vector3(.2, .2, .2); // Match handle height - exitButton.transform.parent = this._toolboxBaseNode; - - exitButton.onPointerObservable.add((evt) => { - this._logger.debug(evt); - if (evt.sourceEvent.type == 'pointerdown') { - this._xr.baseExperience.exitXRAsync(); - } - }); - - // Create config button next to exit button + // Create config button if diagram menu manager is available 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(); - } - }); + this._configButton = new ConfigButton( + () => this._diagramMenuManager.toggleVRConfigPanel(), + this._scene, + this._toolboxBaseNode, + new Vector3(0.5, -0.35, 0) // Bottom-left + ); } - // Create rendering mode button that cycles through modes - this.createRenderModeButton(); + // Create rendering mode button + this._renderModeButton = new RenderModeButton( + this._scene, + this._toolboxBaseNode, + new Vector3(0, -0.2, 0), // Center below grid + new Vector3(0.4, 0.4, 0.4) + ); } }); } - private createRenderModeButton() { - const modes = [ - RenderingMode.LIGHTMAP_WITH_LIGHTING, - RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE, - RenderingMode.FLAT_EMISSIVE, - RenderingMode.DIFFUSE_WITH_LIGHTS - ]; - - const currentMode = LightmapGenerator.getRenderingMode(); - - this._renderModeDisplay = Button.CreateButton( - `Mode: ${RenderingModeLabels[currentMode]}`, - `renderModeButton`, - this._scene, - { - width: 0.5, - height: 0.2, - background: Color3.FromHexString("#333333"), - color: Color3.White(), - fontSize: 240 - } - ); - - // Position below the color grid - this._renderModeDisplay.transform.position.x = 0; - this._renderModeDisplay.transform.position.y = -.2; - this._renderModeDisplay.transform.position.z = 0; - this._renderModeDisplay.transform.rotation.y = Math.PI; - this._renderModeDisplay.transform.scaling = new Vector3(.4, .4, .4); - this._renderModeDisplay.transform.parent = this._toolboxBaseNode; - - // Add click handler to cycle through modes - this._renderModeDisplay.onPointerObservable.add((evt) => { - if (evt.sourceEvent.type == 'pointerdown') { - const currentMode = LightmapGenerator.getRenderingMode(); - const currentIndex = modes.indexOf(currentMode); - const nextIndex = (currentIndex + 1) % modes.length; - const nextMode = modes[nextIndex]; - - this._logger.info(`Cycling to rendering mode: ${nextMode}`); - LightmapGenerator.updateAllMaterials(this._scene, nextMode); - - // Update button text - this.updateRenderModeButton(nextMode); - } - }); - } - - private updateRenderModeButton(mode: RenderingMode) { - if (this._renderModeDisplay) { - // Dispose old button and create new one with updated text - this._renderModeDisplay.dispose(); - - this._renderModeDisplay = Button.CreateButton( - `Mode: ${RenderingModeLabels[mode]}`, - `renderModeButton`, - this._scene, - { - width: 0.5, - height: 0.2, - background: Color3.FromHexString("#333333"), - color: Color3.White(), - fontSize: 240 - } - ); - - this._renderModeDisplay.transform.position.x = 0; - this._renderModeDisplay.transform.position.y = -.2; - this._renderModeDisplay.transform.position.z = 0; - this._renderModeDisplay.transform.rotation.y = Math.PI; - this._renderModeDisplay.transform.scaling = new Vector3(.15, .15, .15); - this._renderModeDisplay.transform.parent = this._toolboxBaseNode; - - // Re-attach the click handler - this._renderModeDisplay.onPointerObservable.add((evt) => { - if (evt.sourceEvent.type == 'pointerdown') { - const modes = [ - RenderingMode.LIGHTMAP_WITH_LIGHTING, - RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE, - RenderingMode.FLAT_EMISSIVE, - RenderingMode.DIFFUSE_WITH_LIGHTS - ]; - - const currentMode = LightmapGenerator.getRenderingMode(); - const currentIndex = modes.indexOf(currentMode); - const nextIndex = (currentIndex + 1) % modes.length; - const nextMode = modes[nextIndex]; - - this._logger.info(`Cycling to rendering mode: ${nextMode}`); - LightmapGenerator.updateAllMaterials(this._scene, nextMode); - - // Update button text - this.updateRenderModeButton(nextMode); - } - }); - } - } }