import {AbstractMesh, Color3, InstancedMesh, Node, Observable, Scene, TransformNode, Vector3, WebXRDefaultExperience} from "@babylonjs/core"; 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"; const colors: string[] = [ "#222222", "#8b4513", "#006400", "#778899", "#4b0082", "#ff0000", "#ffa500", "#ffff00", "#00ff00", "#00ffff", "#0000ff", "#ff00ff", "#1e90ff", "#98fb98", "#ffe4b5", "#ff69b4" ] export class Toolbox { public readonly _toolboxBaseNode: TransformNode; private readonly _tools: Map = new Map(); private readonly _logger = log.getLogger('Toolbox'); private readonly _handle: Handle; private readonly _scene: Scene; private _xr?: WebXRDefaultExperience; constructor(readyObservable: Observable) { this._scene = DefaultScene.Scene; this._toolboxBaseNode = new TransformNode("toolbox", this._scene); this._handle = new Handle(this._toolboxBaseNode, 'Toolbox'); this._toolboxBaseNode.position.y = .2; this._toolboxBaseNode.scaling = new Vector3(0.6, 0.6, 0.6); // Preload lightmaps for all toolbox colors for better first-render performance LightmapGenerator.preloadLightmaps(colors, this._scene); this.buildToolbox().then(() => { readyObservable.notifyObservers(true); this._logger.info('Toolbox built'); }); Toolbox._instance = this; } public setXR(xr: WebXRDefaultExperience): void { this._xr = xr; this.setupXRButton(); } private index = 0; private colorPicker: TransformNode; private changing = false; public static _instance: Toolbox; public static get instance() { return Toolbox._instance; } public get handleMesh(): TransformNode { return this._handle.transformNode; } public isTool(mesh: AbstractMesh) { return this._tools.has(mesh.id); } private async buildToolbox() { this.setupPointerObservable(); await this.buildColorPicker(); if (this._toolboxBaseNode.parent) { const platform = this._scene.getMeshById("platform"); if (platform) { this.assignHandleParentAndStore(platform); } else { const observer = this._scene.onNewMeshAddedObservable.add((mesh: AbstractMesh) => { if (mesh && mesh.id == "platform") { this.assignHandleParentAndStore(mesh); this._scene.onNewMeshAddedObservable.remove(observer); } }, -1, false, this, false); } } } private setupPointerObservable() { this._scene.onPointerObservable.add((pointerInfo) => { const pickedMesh = pointerInfo?.pickInfo?.pickedMesh; if (pointerInfo.type == 1 && pickedMesh?.metadata?.tool == 'color') { if (this.changing) { this._logger.debug('changing'); this.colorPicker.setEnabled(true); return; } else { const active = pickedMesh?.parent.getChildren(this.nodePredicate, true); for (const node of active) { node.setEnabled(false); } const nodes = pickedMesh?.metadata?.tools; if (nodes) { for (const node of nodes) { this._scene.getNodeById(node)?.setEnabled(true); } } } } }); } private nodePredicate = (node: Node) => { return node.getClassName() == "InstancedMesh" && node.isEnabled(false) == true }; private async buildColorPicker() { let initial = true; const colorArray: Promise[] = []; for (const c of colors) { colorArray.push(buildColor(Color3.FromHexString(c), this._scene, this._toolboxBaseNode, this.index++, this._tools)); /*if (initial) { initial = false; for (const id of cnode.metadata.tools) { this._scene.getNodeById(id)?.setEnabled(true); } }*/ } const out = await Promise.all(colorArray); for (const id of out[0].metadata.tools) { this._scene.getNodeById(id)?.setEnabled(true); } } private assignHandleParentAndStore(mesh: TransformNode) { const offset = new Vector3(-.50, 1.6, .38); const rotation = new Vector3(.5, -.6, .18); const handle = this._handle; handle.transformNode.parent = mesh; if (!handle.idStored) { handle.transformNode.position = offset; handle.transformNode.rotation = rotation; } } private setupXRButton() { if (!this._xr) { this._logger.warn('XR not available, exit XR button will not be created'); return; } this._xr.baseExperience.onStateChangedObservable.add((state) => { if (state == 2) { // WebXRState.IN_XR const button = Button.CreateButton("exitXr", "exitXr", this._scene, {}); // Position button at bottom-right of toolbox, matching handle size and orientation button.transform.position.x = 0.5; // Right side button.transform.position.y = -0.35; // Below color grid button.transform.position.z = 0; // Coplanar with toolbox button.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly button.transform.scaling = new Vector3(.2, .2, .2); // Match handle height button.transform.parent = this._toolboxBaseNode; button.onPointerObservable.add((evt) => { this._logger.debug(evt); if (evt.sourceEvent.type == 'pointerdown') { this._xr.baseExperience.exitXRAsync(); } }); } }); } }