import { SpeechInput } from '../../core/speech-input.js';
import { Label } from './label.js';
import { VRSPACEUI } from '../vrspace-ui.js'
/** Menu consisting of vertical buttons in 3D space and associated labels.
*/
export class Buttons {
/**
@param scene babylonjs scene
@param title string displayed above the menu
@param options array of options, string labels or objects
@param callback executed when button is activated
@param property optional, if options are object, this specifies string property to display as label
*/
constructor(scene, title, options, callback, property) {
this.scene = scene;
this.title = title;
this.options = options;
this.callback = callback;
this.property = property;
this.buttonHeight = 1;
this.spacing = 1.1;
/** Can we turn off the current selection? Default false. */
this.turnOff = false;
/** Prefix the button with a number? Default false. */
this.showOptionNumber = false;
this.color = "white";
this.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
this.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
// from here on, state variables:
//this.background = true; //experimental
this.group = new BABYLON.TransformNode("ButtonGroup:" + this.title, scene);
this.groupWidth = 0;
this.buttons = [];
this.selectedOption = -1;
this.controls = [];
this.textures = [];
this.materials = [];
this.pointerTracker = null;
this.speechInput = new SpeechInput();
this.speechInput.addNoMatch((phrases) => this.noMatch(phrases));
//this.display();
}
/** Dispose of everything */
dispose() {
this.scene.onPointerObservable.remove(this.pointerTracker);
delete this.selectedMaterial;
delete this.unselectedMaterial;
this.group.dispose();
this.controls.forEach(e => e.dispose());
this.textures.forEach(e => e.dispose());
this.materials.forEach(e => e.dispose());
this.speechInput.dispose();
console.log("Disposed of buttons " + this.title);
}
/** Set the height, rescales the menu */
setHeight(height) {
let scale = height / this.options.length;
this.group.scaling = new BABYLON.Vector3(scale, scale, scale);
}
/** Called by default on speech recognition mismatch */
noMatch(phrases) {
console.log('no match:', phrases)
}
/** Display the menu, adds a pointer observable */
display() {
// CHECKME: better use emissive color?
this.selectedMaterial = new BABYLON.StandardMaterial("selectedButtonMaterial", this.scene);
this.selectedMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
this.selectedMaterial.emissiveColor = new BABYLON.Color3(.4, .8, .4);
this.selectedMaterial.disableLighting = true;
this.materials.push(this.selectedMaterial);
this.unselectedMaterial = new BABYLON.StandardMaterial("unselectedButtonMaterial", this.scene);
this.unselectedMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
this.unselectedMaterial.emissiveColor = new BABYLON.Color3(.2, .2, .2);
this.unselectedMaterial.disableLighting = true;
this.materials.push(this.unselectedMaterial);
let commandPrefix = "";
if (this.title && this.title.length > 0) {
commandPrefix = this.title + " ";
let height = 1.5;
let titleLabel = new Label(this.title, new BABYLON.Vector3(0, this.spacing * height, 0), this.group);
titleLabel.height = height;
titleLabel.horizontalAlignment = this.horizontalAlignment;
titleLabel.verticalAlignment = this.verticalAlignment;
titleLabel.display();
this.controls.push(titleLabel);
}
for (let i = 0;i < this.options.length;i++) {
if (this.property) {
var option = this.options[i][this.property];
} else {
var option = this.options[i];
}
let command = commandPrefix.toLowerCase();
if (this.showOptionNumber) {
let optionNumber = (i + 1).toString();
option = optionNumber + ". " + option;
this.speechInput.addCommand(command,
(number) => {
if (optionNumber == number && (i != this.selectedOption || this.turnOff)) {
this.select(i);
}
},
"*number"
);
} else {
command += option.toLowerCase();
this.speechInput.addCommand(command,
() => {
if (i != this.selectedOption || this.turnOff) {
this.select(i);
}
}
);
}
this.groupWidth = Math.max(this.groupWidth, option.length);
let buttonLabel = new Label(option, new BABYLON.Vector3(this.buttonHeight, -i * this.spacing, 0), this.group);
buttonLabel.horizontalAlignment = this.horizontalAlignment;
buttonLabel.verticalAlignment = this.verticalAlignment;
buttonLabel.display();
this.controls.push(buttonLabel);
let button = BABYLON.MeshBuilder.CreateCylinder("Button" + option, { height: .1, diameter: this.buttonHeight * .8 }, this.scene);
button.material = this.unselectedMaterial;
button.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);
button.position = new BABYLON.Vector3(this.buttonHeight / 2, -i * this.spacing, 0);
button.parent = this.group;
button.isNearPickable = VRSPACEUI.allowHands;
this.buttons.push(button);
}
this.pointerTracker = (e) => {
if (e.type == BABYLON.PointerEventTypes.POINTERDOWN) {
let p = e.pickInfo;
for (let i = 0;i < this.options.length;i++) {
if (p.pickedMesh == this.buttons[i]) {
// CHECKME we may want to handle double click somehow
if (i != this.selectedOption || this.turnOff) {
this.select(i);
}
break;
}
}
}
};
this.scene.onPointerObservable.add(this.pointerTracker);
// paints background plane, can't be semi-transparent though
if (this.background) {
console.log("Group width: " + this.groupWidth);
let backgroundWidth = this.groupWidth / 1.8;
let backgroundHeight = this.options.length * this.spacing;
let backgroundOffset = this.buttonHeight * .8; // same as button cylinder diameter
let backPlane = BABYLON.MeshBuilder.CreatePlane("ButtonBackground:" + this.title, { height: backgroundHeight, width: backgroundWidth }, this.scene);
backPlane.position = new BABYLON.Vector3(backgroundWidth / 2 + backgroundOffset, -backgroundHeight / 2 + this.spacing / 2, .2);
backPlane.parent = this.group;
let backgroundMaterial = new BABYLON.StandardMaterial("unselectedButtonMaterial", this.scene);
backgroundMaterial.disableLighting = true;
//backgroundMaterial.alpha = 0.5; // produces weird transparency effects
this.materials.push(backgroundMaterial);
backPlane.material = backgroundMaterial;
}
this.speechInput.start();
}
/** Select an option, executed when a button is pressed.
Executes the callback passing selected option as parameter.
*/
select(i) {
console.log("Selected: " + this.options[i].name);
if (this.callback) {
this.callback(this.options[i]);
}
this.buttons[i].material = this.selectedMaterial;
if (this.selectedOption > -1) {
this.buttons[this.selectedOption].material = this.unselectedMaterial;
}
if (i != this.selectedOption) {
this.selectedOption = i;
} else {
this.selectedOption = -1;
}
}
// CHECKME: not used so far
hide() {
this.group.isEnabled = false;
}
show() {
this.group.isEnabled = true;
}
}